Add progress indicator for file upload
This commit is contained in:
parent
717cdb5064
commit
3c4a4fb80b
2 changed files with 55 additions and 22 deletions
|
@ -72,8 +72,9 @@ class RecordApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> uploadAsset(ApiClient client,
|
static Future<void> uploadAsset(ApiClient client,
|
||||||
{required AssetUploadData uploadData, required String filename, required NeosDBAsset asset, required Uint8List data}) async {
|
{required AssetUploadData uploadData, required String filename, required NeosDBAsset asset, required Uint8List data, void Function(double number)? progressCallback}) async {
|
||||||
for (int i = 0; i < uploadData.totalChunks; i++) {
|
for (int i = 0; i < uploadData.totalChunks; i++) {
|
||||||
|
progressCallback?.call(i/uploadData.totalChunks);
|
||||||
final offset = i * uploadData.chunkSize;
|
final offset = i * uploadData.chunkSize;
|
||||||
final end = (i + 1) * uploadData.chunkSize;
|
final end = (i + 1) * uploadData.chunkSize;
|
||||||
final request = http.MultipartRequest(
|
final request = http.MultipartRequest(
|
||||||
|
@ -87,6 +88,7 @@ class RecordApi {
|
||||||
final response = await request.send();
|
final response = await request.send();
|
||||||
final bodyBytes = await response.stream.toBytes();
|
final bodyBytes = await response.stream.toBytes();
|
||||||
ApiClient.checkResponse(http.Response.bytes(bodyBytes, response.statusCode));
|
ApiClient.checkResponse(http.Response.bytes(bodyBytes, response.statusCode));
|
||||||
|
progressCallback?.call(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,18 +97,30 @@ class RecordApi {
|
||||||
ApiClient.checkResponse(response);
|
ApiClient.checkResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> uploadAssets(ApiClient client, {required List<AssetDigest> assets}) async {
|
static Future<void> uploadAssets(ApiClient client, {required List<AssetDigest> assets, void Function(double progress)? progressCallback}) async {
|
||||||
for (final entry in assets) {
|
progressCallback?.call(0);
|
||||||
|
for (int i = 0; i < assets.length; i++) {
|
||||||
|
final totalProgress = i/assets.length;
|
||||||
|
progressCallback?.call(totalProgress);
|
||||||
|
final entry = assets[i];
|
||||||
final uploadData = await beginUploadAsset(client, asset: entry.asset);
|
final uploadData = await beginUploadAsset(client, asset: entry.asset);
|
||||||
if (uploadData.uploadState == UploadState.failed) {
|
if (uploadData.uploadState == UploadState.failed) {
|
||||||
throw "Asset upload failed: ${uploadData.uploadState.name}";
|
throw "Asset upload failed: ${uploadData.uploadState.name}";
|
||||||
}
|
}
|
||||||
await uploadAsset(client, uploadData: uploadData, asset: entry.asset, data: entry.data, filename: entry.name);
|
await uploadAsset(client,
|
||||||
|
uploadData: uploadData,
|
||||||
|
asset: entry.asset,
|
||||||
|
data: entry.data,
|
||||||
|
filename: entry.name,
|
||||||
|
progressCallback: (progress) => progressCallback?.call(totalProgress + progress * 1/assets.length),
|
||||||
|
);
|
||||||
await finishUpload(client, asset: entry.asset);
|
await finishUpload(client, asset: entry.asset);
|
||||||
}
|
}
|
||||||
|
progressCallback?.call(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Record> uploadImage(ApiClient client, {required File image, required String machineId}) async {
|
static Future<Record> uploadImage(ApiClient client, {required File image, required String machineId, void Function(double progress)? progressCallback}) async {
|
||||||
|
progressCallback?.call(0);
|
||||||
final imageDigest = await AssetDigest.fromData(await image.readAsBytes(), basename(image.path));
|
final imageDigest = await AssetDigest.fromData(await image.readAsBytes(), basename(image.path));
|
||||||
final imageData = await decodeImageFromList(imageDigest.data);
|
final imageData = await decodeImageFromList(imageDigest.data);
|
||||||
|
|
||||||
|
@ -128,12 +142,16 @@ class RecordApi {
|
||||||
thumbnailUri: imageDigest.dbUri,
|
thumbnailUri: imageDigest.dbUri,
|
||||||
digests: digests,
|
digests: digests,
|
||||||
);
|
);
|
||||||
|
progressCallback?.call(.1);
|
||||||
final status = await tryPreprocessRecord(client, record: record);
|
final status = await tryPreprocessRecord(client, record: record);
|
||||||
final toUpload = status.resultDiffs.whereNot((element) => element.isUploaded);
|
final toUpload = status.resultDiffs.whereNot((element) => element.isUploaded);
|
||||||
|
progressCallback?.call(.2);
|
||||||
|
|
||||||
await uploadAssets(
|
await uploadAssets(
|
||||||
client, assets: digests.where((digest) => toUpload.any((diff) => digest.asset.hash == diff.hash)).toList());
|
client,
|
||||||
|
assets: digests.where((digest) => toUpload.any((diff) => digest.asset.hash == diff.hash)).toList(),
|
||||||
|
progressCallback: (progress) => progressCallback?.call(.2 + progress * .6));
|
||||||
|
progressCallback?.call(1);
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,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;
|
||||||
|
|
||||||
bool _showBottomBarShadow = false;
|
bool _showBottomBarShadow = false;
|
||||||
bool _showSessionListScrollChevron = false;
|
bool _showSessionListScrollChevron = false;
|
||||||
|
@ -84,8 +85,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendTextMessage(ApiClient client, MessagingClient mClient,
|
Future<void> sendTextMessage(ApiClient client, MessagingClient mClient, String content) async {
|
||||||
String content) async {
|
|
||||||
if (content.isEmpty) return;
|
if (content.isEmpty) return;
|
||||||
final message = Message(
|
final message = Message(
|
||||||
id: Message.generateId(),
|
id: Message.generateId(),
|
||||||
|
@ -97,13 +97,15 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
);
|
);
|
||||||
mClient.sendMessage(message);
|
mClient.sendMessage(message);
|
||||||
_messageTextController.clear();
|
_messageTextController.clear();
|
||||||
|
_hasText = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendImageMessage(ApiClient client, MessagingClient mClient, File file, machineId) async {
|
Future<void> sendImageMessage(ApiClient client, MessagingClient mClient, File file, String machineId, void Function(double progress) progressCallback) async {
|
||||||
final record = await RecordApi.uploadImage(
|
final record = await RecordApi.uploadImage(
|
||||||
client,
|
client,
|
||||||
image: file,
|
image: file,
|
||||||
machineId: machineId,
|
machineId: machineId,
|
||||||
|
progressCallback: progressCallback,
|
||||||
);
|
);
|
||||||
final message = Message(
|
final message = Message(
|
||||||
id: Message.generateId(),
|
id: Message.generateId(),
|
||||||
|
@ -115,6 +117,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
);
|
);
|
||||||
mClient.sendMessage(message);
|
mClient.sendMessage(message);
|
||||||
_messageTextController.clear();
|
_messageTextController.clear();
|
||||||
|
_hasText = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -287,7 +290,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
key: const ValueKey("attachment-picker"),
|
key: const ValueKey("attachment-picker"),
|
||||||
children: [
|
children: [
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () async {
|
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(() {
|
||||||
|
@ -298,7 +301,7 @@ 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: (){}, icon: const Icon(Icons.camera), label: const Text("Camera"),),
|
TextButton.icon(onPressed: _isSending ? null : (){}, icon: const Icon(Icons.camera), label: const Text("Camera"),),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(false, []) => null,
|
(false, []) => null,
|
||||||
|
@ -309,11 +312,11 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: _loadedFiles.map((e) => TextButton.icon(onPressed: (){}, label: Text(basename(e.path)), icon: const Icon(Icons.attach_file))).toList()
|
children: _loadedFiles.map((e) => TextButton.icon(onPressed: _isSending ? null : (){}, label: Text(basename(e.path)), icon: const Icon(Icons.attach_file))).toList()
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(onPressed: () 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(() {
|
||||||
|
@ -321,7 +324,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, icon: const Icon(Icons.image)),
|
}, icon: const Icon(Icons.image)),
|
||||||
IconButton(onPressed: () {}, icon: const Icon(Icons.camera)),
|
IconButton(onPressed: _isSending ? null : () {}, icon: const Icon(Icons.camera)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -331,9 +334,9 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_isSending && _loadedFiles.isNotEmpty)
|
if (_isSending && _loadedFiles.isNotEmpty)
|
||||||
const Align(
|
Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: LinearProgressIndicator(),
|
child: LinearProgressIndicator(value: _sendProgress),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -371,7 +374,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
child: !_attachmentPickerOpen ?
|
child: !_attachmentPickerOpen ?
|
||||||
IconButton(
|
IconButton(
|
||||||
key: const ValueKey("add-attachment-icon"),
|
key: const ValueKey("add-attachment-icon"),
|
||||||
onPressed: () async {
|
onPressed:_isSending ? null : () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_attachmentPickerOpen = true;
|
_attachmentPickerOpen = true;
|
||||||
});
|
});
|
||||||
|
@ -380,7 +383,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
) :
|
) :
|
||||||
IconButton(
|
IconButton(
|
||||||
key: const ValueKey("remove-attachment-icon"),
|
key: const ValueKey("remove-attachment-icon"),
|
||||||
onPressed: () {
|
onPressed: _isSending ? null : () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_loadedFiles.clear();
|
_loadedFiles.clear();
|
||||||
_attachmentPickerOpen = false;
|
_attachmentPickerOpen = false;
|
||||||
|
@ -393,7 +396,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
enabled: cache != null && cache.error == null,
|
enabled: cache != null && cache.error == null && !_isSending,
|
||||||
autocorrect: true,
|
autocorrect: true,
|
||||||
controller: _messageTextController,
|
controller: _messageTextController,
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
|
@ -436,17 +439,29 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
final sMsgnr = ScaffoldMessenger.of(context);
|
final sMsgnr = ScaffoldMessenger.of(context);
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSending = true;
|
_isSending = true;
|
||||||
|
_sendProgress = 0;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
for (final file in _loadedFiles) {
|
for (int i = 0; i < _loadedFiles.length; i++) {
|
||||||
|
final totalProgress = i/_loadedFiles.length;
|
||||||
|
final file = _loadedFiles[i];
|
||||||
await sendImageMessage(apiClient, mClient, file, ClientHolder
|
await sendImageMessage(apiClient, mClient, file, ClientHolder
|
||||||
.of(context)
|
.of(context)
|
||||||
.settingsClient
|
.settingsClient
|
||||||
.currentSettings
|
.currentSettings
|
||||||
.machineId
|
.machineId
|
||||||
.valueOrDefault);
|
.valueOrDefault,
|
||||||
|
(progress) =>
|
||||||
|
setState(() {
|
||||||
|
_sendProgress = totalProgress + progress * 1/_loadedFiles.length;
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_sendProgress = 1;
|
||||||
|
});
|
||||||
|
|
||||||
if (_hasText) {
|
if (_hasText) {
|
||||||
await sendTextMessage(apiClient, mClient, _messageTextController.text);
|
await sendTextMessage(apiClient, mClient, _messageTextController.text);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue