Add progress indicator for file upload

This commit is contained in:
Nutcake 2023-05-18 10:52:32 +02:00
parent 717cdb5064
commit 3c4a4fb80b
2 changed files with 55 additions and 22 deletions

View file

@ -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;
} }
} }

View file

@ -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);
} }