diff --git a/lib/data/repositories/client_repository.dart b/lib/data/repositories/client_repository.dart index 05a7ea10c..5f751ece2 100644 --- a/lib/data/repositories/client_repository.dart +++ b/lib/data/repositories/client_repository.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:core'; import 'package:built_collection/built_collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/data/models/serializers.dart'; import 'package:invoiceninja_flutter/data/web_client.dart'; @@ -80,15 +81,15 @@ class ClientRepository { return clientResponse.data; } - Future uploadDocument( - Credentials credentials, BaseEntity entity, String filePath) async { + Future uploadDocument(Credentials credentials, + BaseEntity entity, MultipartFile multipartFile) async { final fields = { '_method': 'put', }; final dynamic response = await webClient.post( '${credentials.url}/clients/${entity.id}', credentials.token, - data: fields, filePath: filePath, fileIndex: 'documents[]'); + data: fields, multipartFile: multipartFile); final ClientItemResponse clientResponse = serializers.deserializeWith(ClientItemResponse.serializer, response); diff --git a/lib/data/repositories/credit_repository.dart b/lib/data/repositories/credit_repository.dart index 8a5adc112..ea41a1033 100644 --- a/lib/data/repositories/credit_repository.dart +++ b/lib/data/repositories/credit_repository.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:core'; import 'package:built_collection/built_collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/serializers.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; @@ -99,14 +100,14 @@ class CreditRepository { } Future uploadDocument( - Credentials credentials, BaseEntity entity, String filePath) async { + Credentials credentials, BaseEntity entity, MultipartFile multipartFile) async { final fields = { '_method': 'put', }; final dynamic response = await webClient.post( '${credentials.url}/credits/${entity.id}', credentials.token, - data: fields, filePath: filePath, fileIndex: 'documents[]'); + data: fields, multipartFile: multipartFile); final InvoiceItemResponse invoiceResponse = serializers.deserializeWith(InvoiceItemResponse.serializer, response); diff --git a/lib/data/repositories/expense_repository.dart b/lib/data/repositories/expense_repository.dart index 474180bab..e2f49f530 100644 --- a/lib/data/repositories/expense_repository.dart +++ b/lib/data/repositories/expense_repository.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:core'; import 'package:built_collection/built_collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/serializers.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; @@ -74,14 +75,14 @@ class ExpenseRepository { } Future uploadDocument( - Credentials credentials, BaseEntity entity, String filePath) async { + Credentials credentials, BaseEntity entity, MultipartFile multipartFile) async { final fields = { '_method': 'put', }; final dynamic response = await webClient.post( '${credentials.url}/expenses/${entity.id}', credentials.token, - data: fields, filePath: filePath, fileIndex: 'documents[]'); + data: fields, multipartFile: multipartFile); final ExpenseItemResponse expenseResponse = serializers.deserializeWith(ExpenseItemResponse.serializer, response); diff --git a/lib/data/repositories/invoice_repository.dart b/lib/data/repositories/invoice_repository.dart index 47cc42708..f0f831400 100644 --- a/lib/data/repositories/invoice_repository.dart +++ b/lib/data/repositories/invoice_repository.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:core'; import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/serializers.dart'; import 'package:built_collection/built_collection.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -100,14 +101,14 @@ class InvoiceRepository { } Future uploadDocument( - Credentials credentials, BaseEntity entity, String filePath) async { + Credentials credentials, BaseEntity entity, MultipartFile multipartFile) async { final fields = { '_method': 'put', }; final dynamic response = await webClient.post( '${credentials.url}/invoices/${entity.id}', credentials.token, - data: fields, filePath: filePath, fileIndex: 'documents[]'); + data: fields, multipartFile: multipartFile); final InvoiceItemResponse invoiceResponse = serializers.deserializeWith(InvoiceItemResponse.serializer, response); diff --git a/lib/data/repositories/product_repository.dart b/lib/data/repositories/product_repository.dart index c4097c77d..048ce2857 100644 --- a/lib/data/repositories/product_repository.dart +++ b/lib/data/repositories/product_repository.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:core'; import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/serializers.dart'; import 'package:built_collection/built_collection.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -76,7 +77,7 @@ class ProductRepository { } Future uploadDocument( - Credentials credentials, BaseEntity entity, String filePath) async { + Credentials credentials, BaseEntity entity, MultipartFile multipartFile) async { final fields = { '_method': 'put', }; @@ -84,7 +85,7 @@ class ProductRepository { // TODO remove this include final dynamic response = await webClient.post( '${credentials.url}/products/${entity.id}', credentials.token, - data: fields, filePath: filePath, fileIndex: 'documents[]'); + data: fields, multipartFile: multipartFile); final ProductItemResponse productResponse = serializers.deserializeWith(ProductItemResponse.serializer, response); diff --git a/lib/data/repositories/project_repository.dart b/lib/data/repositories/project_repository.dart index 25dd55b55..c5245de50 100644 --- a/lib/data/repositories/project_repository.dart +++ b/lib/data/repositories/project_repository.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:core'; import 'package:built_collection/built_collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/serializers.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; @@ -73,15 +74,15 @@ class ProjectRepository { return projectResponse.data; } - Future uploadDocument( - Credentials credentials, BaseEntity entity, String filePath) async { + Future uploadDocument(Credentials credentials, + BaseEntity entity, MultipartFile multipartFile) async { final fields = { '_method': 'put', }; final dynamic response = await webClient.post( '${credentials.url}/projects/${entity.id}', credentials.token, - data: fields, filePath: filePath, fileIndex: 'documents[]'); + data: fields, multipartFile: multipartFile); final ProjectItemResponse projectResponse = serializers.deserializeWith(ProjectItemResponse.serializer, response); diff --git a/lib/data/repositories/quote_repository.dart b/lib/data/repositories/quote_repository.dart index ebf432167..e4018910d 100644 --- a/lib/data/repositories/quote_repository.dart +++ b/lib/data/repositories/quote_repository.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:core'; import 'package:built_collection/built_collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/serializers.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; @@ -96,14 +97,14 @@ class QuoteRepository { } Future uploadDocument( - Credentials credentials, BaseEntity entity, String filePath) async { + Credentials credentials, BaseEntity entity, MultipartFile multipartFile) async { final fields = { '_method': 'put', }; final dynamic response = await webClient.post( '${credentials.url}/quotes/${entity.id}', credentials.token, - data: fields, filePath: filePath, fileIndex: 'documents[]'); + data: fields, multipartFile: multipartFile); final InvoiceItemResponse invoiceResponse = serializers.deserializeWith(InvoiceItemResponse.serializer, response); diff --git a/lib/data/repositories/settings_repository.dart b/lib/data/repositories/settings_repository.dart index d6ea94fa3..269afceb5 100644 --- a/lib/data/repositories/settings_repository.dart +++ b/lib/data/repositories/settings_repository.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:core'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/group_model.dart'; import 'package:invoiceninja_flutter/data/models/serializers.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -67,7 +68,7 @@ class SettingsRepository { } Future uploadLogo(Credentials credentials, String entityId, - String path, EntityType type) async { + MultipartFile multipartFile, EntityType type) async { final route = type == EntityType.company ? 'companies' : type == EntityType.group @@ -76,7 +77,7 @@ class SettingsRepository { final url = '${credentials.url}/$route/$entityId'; final dynamic response = await webClient.post(url, credentials.token, - data: {'_method': 'PUT'}, filePath: path, fileIndex: 'company_logo'); + data: {'_method': 'PUT'}, multipartFile: multipartFile); if (type == EntityType.client) { return serializers diff --git a/lib/data/repositories/task_repository.dart b/lib/data/repositories/task_repository.dart index f9ab43eda..9994ebd97 100644 --- a/lib/data/repositories/task_repository.dart +++ b/lib/data/repositories/task_repository.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:core'; import 'package:built_collection/built_collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/serializers.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; @@ -73,14 +74,14 @@ class TaskRepository { } Future uploadDocument( - Credentials credentials, BaseEntity entity, String filePath) async { + Credentials credentials, BaseEntity entity, MultipartFile multipartFile) async { final fields = { '_method': 'put', }; final dynamic response = await webClient.post( '${credentials.url}/tasks/${entity.id}', credentials.token, - data: fields, filePath: filePath, fileIndex: 'documents[]'); + data: fields, multipartFile: multipartFile); final TaskItemResponse taskResponse = serializers.deserializeWith(TaskItemResponse.serializer, response); diff --git a/lib/data/repositories/vendor_repository.dart b/lib/data/repositories/vendor_repository.dart index 0a9043d89..70de98c3d 100644 --- a/lib/data/repositories/vendor_repository.dart +++ b/lib/data/repositories/vendor_repository.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:core'; import 'package:built_collection/built_collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/serializers.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; @@ -74,14 +75,14 @@ class VendorRepository { } Future uploadDocument( - Credentials credentials, BaseEntity entity, String filePath) async { + Credentials credentials, BaseEntity entity, MultipartFile multipartFile) async { final fields = { '_method': 'put', }; final dynamic response = await webClient.post( '${credentials.url}/vendors/${entity.id}', credentials.token, - data: fields, filePath: filePath, fileIndex: 'documents[]'); + data: fields, multipartFile: multipartFile); final VendorItemResponse vendorResponse = serializers.deserializeWith(VendorItemResponse.serializer, response); diff --git a/lib/data/web_client.dart b/lib/data/web_client.dart index 529107be8..91fbd2efb 100644 --- a/lib/data/web_client.dart +++ b/lib/data/web_client.dart @@ -1,13 +1,12 @@ import 'dart:async'; import 'dart:convert'; import 'dart:core'; -import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/.env.dart'; import 'package:http/http.dart' as http; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/utils/strings.dart'; -import 'package:path/path.dart'; import 'package:version/version.dart'; class WebClient { @@ -55,8 +54,7 @@ class WebClient { String url, String token, { dynamic data, - String filePath, - String fileIndex, + MultipartFile multipartFile, String secret, String password, bool rawResponse = false, @@ -75,9 +73,8 @@ class WebClient { } http.Response response; - if (filePath != null) { - response = await _uploadFile(url, token, filePath, - fileIndex: fileIndex, data: data); + if (multipartFile != null) { + response = await _uploadFile(url, token, multipartFile, data: data); } else { response = await http.Client() .post(url, @@ -100,7 +97,7 @@ class WebClient { String url, String token, { dynamic data, - String filePath, + MultipartFile multipartFile, String fileIndex = 'file', String password, }) async { @@ -118,8 +115,8 @@ class WebClient { } http.Response response; - if (filePath != null) { - response = await _uploadFile(url, token, filePath, + if (multipartFile != null) { + response = await _uploadFile(url, token, multipartFile, fileIndex: fileIndex, data: data, method: 'PUT'); } else { response = await http.Client().put( @@ -238,27 +235,9 @@ String _parseError(int code, String response) { return '$code: $message'; } -Future _uploadFile(String url, String token, String filePath, +Future _uploadFile( + String url, String token, MultipartFile multipartFile, {String method = 'POST', String fileIndex = 'file', dynamic data}) async { - dynamic multipartFile; - - if (filePath.startsWith('data:')) { - final parts = filePath.split(','); - final prefix = parts[0]; - final startIndex = prefix.indexOf('/') + 1; - final endIndex = prefix.indexOf(';'); - final fileExt = prefix.substring(startIndex, endIndex); - final bytes = base64.decode(parts[1]); - multipartFile = http.MultipartFile.fromBytes(fileIndex, bytes, - filename: 'file.$fileExt'); - } else { - final file = File(filePath); - final stream = http.ByteStream(file.openRead().cast()); - final length = await file.length(); - multipartFile = http.MultipartFile(fileIndex, stream, length, - filename: basename(file.path)); - } - final request = http.MultipartRequest(method, Uri.parse(url)) ..fields.addAll(data ?? {}) ..headers.addAll(_getHeaders(url, token)) diff --git a/lib/redux/client/client_actions.dart b/lib/redux/client/client_actions.dart index 35fcc4759..1f430b135 100644 --- a/lib/redux/client/client_actions.dart +++ b/lib/redux/client/client_actions.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:built_collection/built_collection.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; @@ -434,12 +435,12 @@ class ClearClientMultiselect {} class SaveClientDocumentRequest implements StartSaving { SaveClientDocumentRequest({ @required this.completer, - @required this.filePath, + @required this.multipartFile, @required this.client, }); final Completer completer; - final String filePath; + final MultipartFile multipartFile; final ClientEntity client; } diff --git a/lib/redux/client/client_middleware.dart b/lib/redux/client/client_middleware.dart index 827f2fa62..5eca0d3c7 100644 --- a/lib/redux/client/client_middleware.dart +++ b/lib/redux/client/client_middleware.dart @@ -251,7 +251,7 @@ Middleware _saveDocument(ClientRepository repository) { if (store.state.isEnterprisePlan) { repository .uploadDocument( - store.state.credentials, action.client, action.filePath) + store.state.credentials, action.client, action.multipartFile) .then((client) { store.dispatch(SaveClientSuccess(client)); action.completer.complete(null); diff --git a/lib/redux/credit/credit_actions.dart b/lib/redux/credit/credit_actions.dart index 3e8878f78..2c054c5bf 100644 --- a/lib/redux/credit/credit_actions.dart +++ b/lib/redux/credit/credit_actions.dart @@ -4,6 +4,7 @@ import 'package:built_collection/built_collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -374,12 +375,12 @@ class FilterCreditsByCustom4 implements PersistUI { class SaveCreditDocumentRequest implements StartSaving { SaveCreditDocumentRequest({ @required this.completer, - @required this.filePath, + @required this.multipartFile, @required this.credit, }); final Completer completer; - final String filePath; + final MultipartFile multipartFile; final InvoiceEntity credit; } diff --git a/lib/redux/credit/credit_middleware.dart b/lib/redux/credit/credit_middleware.dart index ce2b8a3ca..8c314daf7 100644 --- a/lib/redux/credit/credit_middleware.dart +++ b/lib/redux/credit/credit_middleware.dart @@ -348,7 +348,7 @@ Middleware _saveDocument(CreditRepository repository) { if (store.state.isEnterprisePlan) { repository .uploadDocument( - store.state.credentials, action.credit, action.filePath) + store.state.credentials, action.credit, action.multipartFile) .then((credit) { store.dispatch(SaveCreditSuccess(credit)); action.completer.complete(null); diff --git a/lib/redux/document/document_actions.dart b/lib/redux/document/document_actions.dart index 25d445e1a..886f0ca9f 100644 --- a/lib/redux/document/document_actions.dart +++ b/lib/redux/document/document_actions.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:built_collection/built_collection.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -115,12 +116,12 @@ class LoadDocumentsSuccess implements StopLoading { class SaveDocumentRequest implements StartSaving { SaveDocumentRequest({ @required this.completer, - @required this.filePath, + @required this.multipartFile, @required this.entity, }); final Completer completer; - final String filePath; + final MultipartFile multipartFile; final BaseEntity entity; } diff --git a/lib/redux/expense/expense_actions.dart b/lib/redux/expense/expense_actions.dart index f2ee1cb28..b6e8a80a4 100644 --- a/lib/redux/expense/expense_actions.dart +++ b/lib/redux/expense/expense_actions.dart @@ -4,6 +4,7 @@ import 'package:built_collection/built_collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -356,12 +357,12 @@ class ClearExpenseMultiselect {} class SaveExpenseDocumentRequest implements StartSaving { SaveExpenseDocumentRequest({ @required this.completer, - @required this.filePath, + @required this.multipartFile, @required this.expense, }); final Completer completer; - final String filePath; + final MultipartFile multipartFile; final ExpenseEntity expense; } diff --git a/lib/redux/expense/expense_middleware.dart b/lib/redux/expense/expense_middleware.dart index 282517a78..d97ea5d73 100644 --- a/lib/redux/expense/expense_middleware.dart +++ b/lib/redux/expense/expense_middleware.dart @@ -247,7 +247,7 @@ Middleware _saveDocument(ExpenseRepository repository) { if (store.state.isEnterprisePlan) { repository .uploadDocument( - store.state.credentials, action.expense, action.filePath) + store.state.credentials, action.expense, action.multipartFile) .then((expense) { store.dispatch(SaveExpenseSuccess(expense)); action.completer.complete(null); diff --git a/lib/redux/invoice/invoice_actions.dart b/lib/redux/invoice/invoice_actions.dart index 81478ef99..be9416c45 100644 --- a/lib/redux/invoice/invoice_actions.dart +++ b/lib/redux/invoice/invoice_actions.dart @@ -3,6 +3,7 @@ import 'package:built_collection/built_collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -445,12 +446,12 @@ class ClearInvoiceMultiselect {} class SaveInvoiceDocumentRequest implements StartSaving { SaveInvoiceDocumentRequest({ @required this.completer, - @required this.filePath, + @required this.multipartFile, @required this.invoice, }); final Completer completer; - final String filePath; + final MultipartFile multipartFile; final InvoiceEntity invoice; } diff --git a/lib/redux/invoice/invoice_middleware.dart b/lib/redux/invoice/invoice_middleware.dart index 7d5bc7aa2..3b78a791b 100644 --- a/lib/redux/invoice/invoice_middleware.dart +++ b/lib/redux/invoice/invoice_middleware.dart @@ -432,7 +432,7 @@ Middleware _saveDocument(InvoiceRepository repository) { if (store.state.isEnterprisePlan) { repository .uploadDocument( - store.state.credentials, action.invoice, action.filePath) + store.state.credentials, action.invoice, action.multipartFile) .then((invoice) { store.dispatch(SaveInvoiceSuccess(invoice)); action.completer.complete(null); diff --git a/lib/redux/product/product_actions.dart b/lib/redux/product/product_actions.dart index d72d48c91..e5b95373c 100644 --- a/lib/redux/product/product_actions.dart +++ b/lib/redux/product/product_actions.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:built_collection/built_collection.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -349,12 +350,12 @@ class ClearProductMultiselect {} class SaveProductDocumentRequest implements StartSaving { SaveProductDocumentRequest({ @required this.completer, - @required this.filePath, + @required this.multipartFile, @required this.product, }); final Completer completer; - final String filePath; + final MultipartFile multipartFile; final ProductEntity product; } diff --git a/lib/redux/product/product_middleware.dart b/lib/redux/product/product_middleware.dart index 3b84dccf3..45de09737 100644 --- a/lib/redux/product/product_middleware.dart +++ b/lib/redux/product/product_middleware.dart @@ -248,7 +248,7 @@ Middleware _saveDocument(ProductRepository repository) { if (store.state.isEnterprisePlan) { repository .uploadDocument( - store.state.credentials, action.product, action.filePath) + store.state.credentials, action.product, action.multipartFile) .then((product) { store.dispatch(SaveProductSuccess(product)); action.completer.complete(null); diff --git a/lib/redux/project/project_actions.dart b/lib/redux/project/project_actions.dart index 2db20e647..a96226cdb 100644 --- a/lib/redux/project/project_actions.dart +++ b/lib/redux/project/project_actions.dart @@ -4,6 +4,7 @@ import 'package:built_collection/built_collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -367,12 +368,12 @@ class ClearProjectMultiselect {} class SaveProjectDocumentRequest implements StartSaving { SaveProjectDocumentRequest({ @required this.completer, - @required this.filePath, + @required this.multipartFile, @required this.project, }); final Completer completer; - final String filePath; + final MultipartFile multipartFile; final ProjectEntity project; } diff --git a/lib/redux/project/project_middleware.dart b/lib/redux/project/project_middleware.dart index 2dfa42b74..8385e7f50 100644 --- a/lib/redux/project/project_middleware.dart +++ b/lib/redux/project/project_middleware.dart @@ -254,7 +254,7 @@ Middleware _saveDocument(ProjectRepository repository) { if (store.state.isEnterprisePlan) { repository .uploadDocument( - store.state.credentials, action.project, action.filePath) + store.state.credentials, action.project, action.multipartFile) .then((project) { store.dispatch(SaveProjectSuccess(project)); action.completer.complete(null); diff --git a/lib/redux/quote/quote_actions.dart b/lib/redux/quote/quote_actions.dart index 46e37f31a..3fe9c44cf 100644 --- a/lib/redux/quote/quote_actions.dart +++ b/lib/redux/quote/quote_actions.dart @@ -4,6 +4,7 @@ import 'package:built_collection/built_collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -393,12 +394,12 @@ class ConvertQuoteFailure implements StopSaving { class SaveQuoteDocumentRequest implements StartSaving { SaveQuoteDocumentRequest({ @required this.completer, - @required this.filePath, + @required this.multipartFile, @required this.quote, }); final Completer completer; - final String filePath; + final MultipartFile multipartFile; final InvoiceEntity quote; } diff --git a/lib/redux/quote/quote_middleware.dart b/lib/redux/quote/quote_middleware.dart index 88aa60507..bc0415fbe 100644 --- a/lib/redux/quote/quote_middleware.dart +++ b/lib/redux/quote/quote_middleware.dart @@ -367,7 +367,7 @@ Middleware _saveDocument(QuoteRepository repository) { if (store.state.isEnterprisePlan) { repository .uploadDocument( - store.state.credentials, action.quote, action.filePath) + store.state.credentials, action.quote, action.multipartFile) .then((quote) { store.dispatch(SaveQuoteSuccess(quote)); action.completer.complete(null); diff --git a/lib/redux/recurring_invoice/recurring_invoice_actions.dart b/lib/redux/recurring_invoice/recurring_invoice_actions.dart index d8d09eb9c..3f2123f7c 100644 --- a/lib/redux/recurring_invoice/recurring_invoice_actions.dart +++ b/lib/redux/recurring_invoice/recurring_invoice_actions.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:built_collection/built_collection.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -347,12 +348,12 @@ class FilterRecurringInvoicesByCustom4 implements PersistUI { class SaveRecurringInvoiceDocumentRequest implements StartSaving { SaveRecurringInvoiceDocumentRequest({ @required this.completer, - @required this.filePath, + @required this.multipartFile, @required this.invoice, }); final Completer completer; - final String filePath; + final MultipartFile multipartFile; final InvoiceEntity invoice; } diff --git a/lib/redux/settings/settings_actions.dart b/lib/redux/settings/settings_actions.dart index 2b71fd337..0163bc9a6 100644 --- a/lib/redux/settings/settings_actions.dart +++ b/lib/redux/settings/settings_actions.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/client_model.dart'; import 'package:invoiceninja_flutter/data/models/company_model.dart'; import 'package:invoiceninja_flutter/data/models/entities.dart'; @@ -52,10 +53,10 @@ class UpdateUserSettings implements PersistUI { } class UploadLogoRequest implements StartSaving { - UploadLogoRequest({this.completer, this.path, this.type}); + UploadLogoRequest({this.completer, this.multipartFile, this.type}); final Completer completer; - final String path; + final MultipartFile multipartFile; final EntityType type; } diff --git a/lib/redux/settings/settings_middleware.dart b/lib/redux/settings/settings_middleware.dart index b26eea845..68e36b036 100644 --- a/lib/redux/settings/settings_middleware.dart +++ b/lib/redux/settings/settings_middleware.dart @@ -144,7 +144,7 @@ Middleware _uploadLogo(SettingsRepository settingsRepository) { ? settingsState.group.id : settingsState.client.id; settingsRepository - .uploadLogo(store.state.credentials, entityId, action.path, action.type) + .uploadLogo(store.state.credentials, entityId, action.multipartFile, action.type) .then((entity) { if (action.type == EntityType.client) { store.dispatch(SaveClientSuccess(entity as ClientEntity)); diff --git a/lib/redux/task/task_actions.dart b/lib/redux/task/task_actions.dart index 3e1a3f922..b21014b7b 100644 --- a/lib/redux/task/task_actions.dart +++ b/lib/redux/task/task_actions.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -414,12 +415,12 @@ class ClearTaskMultiselect {} class SaveTaskDocumentRequest implements StartSaving { SaveTaskDocumentRequest({ @required this.completer, - @required this.filePath, + @required this.multipartFile, @required this.task, }); final Completer completer; - final String filePath; + final MultipartFile multipartFile; final TaskEntity task; } diff --git a/lib/redux/task/task_middleware.dart b/lib/redux/task/task_middleware.dart index 72d116305..d9888609f 100644 --- a/lib/redux/task/task_middleware.dart +++ b/lib/redux/task/task_middleware.dart @@ -242,7 +242,7 @@ Middleware _saveDocument(TaskRepository repository) { final action = dynamicAction as SaveTaskDocumentRequest; if (store.state.isEnterprisePlan) { repository - .uploadDocument(store.state.credentials, action.task, action.filePath) + .uploadDocument(store.state.credentials, action.task, action.multipartFile) .then((task) { store.dispatch(SaveTaskSuccess(task)); action.completer.complete(null); diff --git a/lib/redux/vendor/vendor_actions.dart b/lib/redux/vendor/vendor_actions.dart index a414eb92e..ccbd9a739 100644 --- a/lib/redux/vendor/vendor_actions.dart +++ b/lib/redux/vendor/vendor_actions.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:built_collection/built_collection.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -373,12 +374,12 @@ class ClearVendorMultiselect {} class SaveVendorDocumentRequest implements StartSaving { SaveVendorDocumentRequest({ @required this.completer, - @required this.filePath, + @required this.multipartFile, @required this.vendor, }); final Completer completer; - final String filePath; + final MultipartFile multipartFile; final VendorEntity vendor; } diff --git a/lib/redux/vendor/vendor_middleware.dart b/lib/redux/vendor/vendor_middleware.dart index 31f0d98da..bda491009 100644 --- a/lib/redux/vendor/vendor_middleware.dart +++ b/lib/redux/vendor/vendor_middleware.dart @@ -251,7 +251,7 @@ Middleware _saveDocument(VendorRepository repository) { if (store.state.isEnterprisePlan) { repository .uploadDocument( - store.state.credentials, action.vendor, action.filePath) + store.state.credentials, action.vendor, action.multipartFile) .then((vendor) { store.dispatch(SaveVendorSuccess(vendor)); action.completer.complete(null); diff --git a/lib/ui/app/document_grid.dart b/lib/ui/app/document_grid.dart index 11f84664d..6cfa9c313 100644 --- a/lib/ui/app/document_grid.dart +++ b/lib/ui/app/document_grid.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_share/flutter_share.dart'; +import 'package:http/http.dart'; import 'package:image_picker/image_picker.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/data/web_client.dart'; @@ -12,12 +13,11 @@ import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/ui/app/buttons/elevated_button.dart'; import 'package:invoiceninja_flutter/ui/app/lists/list_divider.dart'; import 'package:invoiceninja_flutter/utils/dialogs.dart'; +import 'package:invoiceninja_flutter/utils/files.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/icons.dart'; import 'package:http/http.dart' as http; import 'package:invoiceninja_flutter/utils/localization.dart'; -import 'package:invoiceninja_flutter/utils/web_stub.dart' - if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -31,7 +31,7 @@ class DocumentGrid extends StatelessWidget { }); final List documents; - final Function(String) onUploadDocument; + final Function(MultipartFile) onUploadDocument; final Function(DocumentEntity, String) onDeleteDocument; final Function(DocumentEntity) onViewExpense; @@ -64,7 +64,11 @@ class DocumentGrid extends StatelessWidget { final image = await ImagePicker() .getImage(source: ImageSource.camera); if (image != null && image.path != null) { - onUploadDocument(image.path); + final bytes = await image.readAsBytes(); + final multipartFile = MultipartFile.fromBytes( + 'documents[]', bytes, + filename: image.path.split('/').last); + onUploadDocument(multipartFile); } } else { openAppSettings(); @@ -81,29 +85,9 @@ class DocumentGrid extends StatelessWidget { iconData: Icons.insert_drive_file, label: localization.uploadFile, onPressed: () async { - String path; - if (kIsWeb) { - path = await WebUtils.filePicker(); - } else { - final permissionStatus = - await [Permission.photos].request(); - final permission = - permissionStatus[Permission.photos] ?? - PermissionStatus.undetermined; - - if (permission == PermissionStatus.granted) { - final image = await ImagePicker() - .getImage(source: ImageSource.gallery); - if (image != null) { - path = image.path; - } - } else { - openAppSettings(); - } - } - if (path != null) { - onUploadDocument(path); - } + final multipartFile = + await pickFile(fileIndex: 'documents[]'); + onUploadDocument(multipartFile); }, ), ), diff --git a/lib/ui/client/view/client_view_vm.dart b/lib/ui/client/view/client_view_vm.dart index c600a54ed..6230b044c 100644 --- a/lib/ui/client/view/client_view_vm.dart +++ b/lib/ui/client/view/client_view_vm.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -80,10 +81,10 @@ class ClientViewVM { onRefreshed: (context) => _handleRefresh(context), onEntityAction: (BuildContext context, EntityAction action) => handleEntitiesActions(context, [client], action, autoPop: true), - onUploadDocument: (BuildContext context, String filePath) { + onUploadDocument: (BuildContext context, MultipartFile multipartFile) { final Completer completer = Completer(); store.dispatch(SaveClientDocumentRequest( - filePath: filePath, client: client, completer: completer)); + multipartFile: multipartFile, client: client, completer: completer)); completer.future.then((client) { showToast(AppLocalization.of(context).uploadedDocument); }).catchError((Object error) { @@ -114,7 +115,7 @@ class ClientViewVM { final CompanyEntity company; final Function(BuildContext, EntityAction) onEntityAction; final Function(BuildContext) onRefreshed; - final Function(BuildContext, String) onUploadDocument; + final Function(BuildContext, MultipartFile) onUploadDocument; final Function(BuildContext, DocumentEntity, String) onDeleteDocument; final bool isSaving; final bool isLoading; diff --git a/lib/ui/credit/view/credit_view_vm.dart b/lib/ui/credit/view/credit_view_vm.dart index 21ec140f1..e82e47f25 100644 --- a/lib/ui/credit/view/credit_view_vm.dart +++ b/lib/ui/credit/view/credit_view_vm.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -56,7 +57,7 @@ class CreditViewVM extends EntityViewVM { Function(BuildContext) onPaymentsPressed, Function(BuildContext, PaymentEntity) onPaymentPressed, Function(BuildContext) onRefreshed, - Function(BuildContext, String) onUploadDocument, + Function(BuildContext, MultipartFile) onUploadDocument, Function(BuildContext, DocumentEntity, String) onDeleteDocument, Function(BuildContext, DocumentEntity) onViewExpense, }) : super( @@ -107,10 +108,10 @@ class CreditViewVM extends EntityViewVM { onRefreshed: (context) => _handleRefresh(context), onEntityAction: (BuildContext context, EntityAction action) => handleEntitiesActions(context, [credit], action, autoPop: true), - onUploadDocument: (BuildContext context, String filePath) { + onUploadDocument: (BuildContext context, MultipartFile multipartFile) { final Completer completer = Completer(); store.dispatch(SaveCreditDocumentRequest( - filePath: filePath, credit: credit, completer: completer)); + multipartFile: multipartFile, credit: credit, completer: completer)); completer.future.then((client) { showToast(AppLocalization.of(context).uploadedDocument); }).catchError((Object error) { diff --git a/lib/ui/expense/view/expense_view.dart b/lib/ui/expense/view/expense_view.dart index 120f9dd68..f0199361f 100644 --- a/lib/ui/expense/view/expense_view.dart +++ b/lib/ui/expense/view/expense_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:http/http.dart'; import 'package:image_picker/image_picker.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/ui/app/buttons/bottom_buttons.dart'; @@ -7,9 +8,9 @@ import 'package:invoiceninja_flutter/ui/app/view_scaffold.dart'; import 'package:invoiceninja_flutter/ui/expense/view/expense_view_documents.dart'; import 'package:invoiceninja_flutter/ui/expense/view/expense_view_vm.dart'; import 'package:invoiceninja_flutter/ui/expense/view/expense_view_overview.dart'; +import 'package:invoiceninja_flutter/utils/files.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; -import 'package:invoiceninja_flutter/utils/web_stub.dart' - if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; +import 'package:permission_handler/permission_handler.dart'; class ExpenseView extends StatefulWidget { const ExpenseView({ @@ -101,18 +102,30 @@ class _ExpenseViewState extends State heroTag: 'expense_fab', backgroundColor: Theme.of(context).primaryColorDark, onPressed: () async { - String path; + MultipartFile multipartFile; if (kIsWeb) { - path = await WebUtils.filePicker(); + multipartFile = await pickFile(); } else { - final image = await ImagePicker() - .getImage(source: ImageSource.camera); - if (image != null) { - path = image.path; + final permissionStatus = + await [Permission.camera].request(); + final permission = permissionStatus[Permission.camera] ?? + PermissionStatus.undetermined; + + if (permission == PermissionStatus.granted) { + final image = await ImagePicker() + .getImage(source: ImageSource.camera); + if (image != null && image.path != null) { + final bytes = await image.readAsBytes(); + multipartFile = MultipartFile.fromBytes( + 'file', bytes, + filename: image.path.split('/').last); + } + } else { + openAppSettings(); } } - if (path != null) { - viewModel.onUploadDocument(context, path); + if (multipartFile != null) { + viewModel.onUploadDocument(context, multipartFile); } }, child: Icon( diff --git a/lib/ui/expense/view/expense_view_vm.dart b/lib/ui/expense/view/expense_view_vm.dart index 871eedf3c..8567c7631 100644 --- a/lib/ui/expense/view/expense_view_vm.dart +++ b/lib/ui/expense/view/expense_view_vm.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/expense_model.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; @@ -111,11 +112,11 @@ class ExpenseViewVM { }, onEntityAction: (BuildContext context, EntityAction action) => handleEntitiesActions(context, [expense], action, autoPop: true), - onUploadDocument: (BuildContext context, String filePath) { + onUploadDocument: (BuildContext context, MultipartFile multipartFile) { final Completer completer = Completer(); store.dispatch(SaveDocumentRequest( - filePath: filePath, entity: expense, completer: completer)); + multipartFile: multipartFile, entity: expense, completer: completer)); completer.future.then((client) { showToast(AppLocalization.of(context).uploadedDocument); }).catchError((Object error) { @@ -143,7 +144,7 @@ class ExpenseViewVM { final Function(BuildContext, EntityAction) onEntityAction; final Function(BuildContext, EntityType, [bool]) onEntityPressed; final Function(BuildContext) onRefreshed; - final Function(BuildContext, String) onUploadDocument; + final Function(BuildContext, MultipartFile) onUploadDocument; final Function(BuildContext, DocumentEntity, String) onDeleteDocument; final bool isSaving; final bool isLoading; diff --git a/lib/ui/invoice/view/invoice_view_vm.dart b/lib/ui/invoice/view/invoice_view_vm.dart index 89979e066..20eebe9c5 100644 --- a/lib/ui/invoice/view/invoice_view_vm.dart +++ b/lib/ui/invoice/view/invoice_view_vm.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -66,7 +67,7 @@ class EntityViewVM { final Function(BuildContext, [int]) onEditPressed; final Function(BuildContext) onPaymentsPressed; final Function(BuildContext) onRefreshed; - final Function(BuildContext, String) onUploadDocument; + final Function(BuildContext, MultipartFile) onUploadDocument; final Function(BuildContext, DocumentEntity, String) onDeleteDocument; final Function(BuildContext, DocumentEntity) onViewExpense; } @@ -86,7 +87,7 @@ class InvoiceViewVM extends EntityViewVM { Function(BuildContext, PaymentEntity, [bool]) onPaymentPressed, Function(BuildContext) onPaymentsPressed, Function(BuildContext) onRefreshed, - Function(BuildContext, String) onUploadDocument, + Function(BuildContext, MultipartFile) onUploadDocument, Function(BuildContext, DocumentEntity, String) onDeleteDocument, Function(BuildContext, DocumentEntity) onViewExpense, }) : super( @@ -140,10 +141,10 @@ class InvoiceViewVM extends EntityViewVM { }, onEntityAction: (BuildContext context, EntityAction action) => handleEntitiesActions(context, [invoice], action, autoPop: true), - onUploadDocument: (BuildContext context, String filePath) { + onUploadDocument: (BuildContext context, MultipartFile multipartFile) { final Completer completer = Completer(); store.dispatch(SaveInvoiceDocumentRequest( - filePath: filePath, invoice: invoice, completer: completer)); + multipartFile: multipartFile, invoice: invoice, completer: completer)); completer.future.then((client) { showToast(AppLocalization.of(context).uploadedDocument); }).catchError((Object error) { diff --git a/lib/ui/product/view/product_view_vm.dart b/lib/ui/product/view/product_view_vm.dart index 6bccb657e..e5fe2aeaa 100644 --- a/lib/ui/product/view/product_view_vm.dart +++ b/lib/ui/product/view/product_view_vm.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -79,10 +80,10 @@ class ProductViewVM { onRefreshed: (context) => _handleRefresh(context), onEntityAction: (BuildContext context, EntityAction action) => handleEntitiesActions(context, [product], action, autoPop: true), - onUploadDocument: (BuildContext context, String filePath) { + onUploadDocument: (BuildContext context, MultipartFile multipartFile) { final Completer completer = Completer(); store.dispatch(SaveProductDocumentRequest( - filePath: filePath, product: product, completer: completer)); + multipartFile: multipartFile, product: product, completer: completer)); completer.future.then((client) { showToast(AppLocalization.of(context).uploadedDocument); }).catchError((Object error) { @@ -113,7 +114,7 @@ class ProductViewVM { final CompanyEntity company; final Function(BuildContext, EntityAction) onEntityAction; final Function(BuildContext) onRefreshed; - final Function(BuildContext, String) onUploadDocument; + final Function(BuildContext, MultipartFile) onUploadDocument; final Function(BuildContext, DocumentEntity, String) onDeleteDocument; final bool isSaving; final bool isLoading; diff --git a/lib/ui/project/view/project_view_vm.dart b/lib/ui/project/view/project_view_vm.dart index 23d4e208c..bedafed8a 100644 --- a/lib/ui/project/view/project_view_vm.dart +++ b/lib/ui/project/view/project_view_vm.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/data/models/project_model.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; @@ -100,10 +101,10 @@ class ProjectViewVM { }, onEntityAction: (BuildContext context, EntityAction action) => handleEntitiesActions(context, [project], action, autoPop: true), - onUploadDocument: (BuildContext context, String filePath) { + onUploadDocument: (BuildContext context, MultipartFile multipartFile) { final Completer completer = Completer(); store.dispatch(SaveProjectDocumentRequest( - filePath: filePath, project: project, completer: completer)); + multipartFile: multipartFile, project: project, completer: completer)); completer.future.then((client) { showToast(AppLocalization.of(context).uploadedDocument); }).catchError((Object error) { @@ -140,6 +141,6 @@ class ProjectViewVM { final bool isSaving; final bool isLoading; final bool isDirty; - final Function(BuildContext, String) onUploadDocument; + final Function(BuildContext, MultipartFile) onUploadDocument; final Function(BuildContext, DocumentEntity, String) onDeleteDocument; } diff --git a/lib/ui/quote/view/quote_view_vm.dart b/lib/ui/quote/view/quote_view_vm.dart index 15f212786..91ea510e3 100644 --- a/lib/ui/quote/view/quote_view_vm.dart +++ b/lib/ui/quote/view/quote_view_vm.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -54,7 +55,7 @@ class QuoteViewVM extends EntityViewVM { Function(BuildContext) onPaymentsPressed, Function(BuildContext, PaymentEntity) onPaymentPressed, Function(BuildContext) onRefreshed, - Function(BuildContext, String) onUploadDocument, + Function(BuildContext, MultipartFile) onUploadDocument, Function(BuildContext, DocumentEntity, String) onDeleteDocument, Function(BuildContext, DocumentEntity) onViewExpense, }) : super( @@ -105,10 +106,10 @@ class QuoteViewVM extends EntityViewVM { onRefreshed: (context) => _handleRefresh(context), onEntityAction: (BuildContext context, EntityAction action) => handleEntitiesActions(context, [quote], action, autoPop: true), - onUploadDocument: (BuildContext context, String filePath) { + onUploadDocument: (BuildContext context, MultipartFile multipartFile) { final Completer completer = Completer(); store.dispatch(SaveQuoteDocumentRequest( - filePath: filePath, quote: quote, completer: completer)); + multipartFile: multipartFile, quote: quote, completer: completer)); completer.future.then((client) { showToast(AppLocalization.of(context).uploadedDocument); }).catchError((Object error) { diff --git a/lib/ui/recurring_invoice/view/recurring_invoice_view_vm.dart b/lib/ui/recurring_invoice/view/recurring_invoice_view_vm.dart index ba40ccbb8..88271a8b3 100644 --- a/lib/ui/recurring_invoice/view/recurring_invoice_view_vm.dart +++ b/lib/ui/recurring_invoice/view/recurring_invoice_view_vm.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -53,7 +54,7 @@ class RecurringInvoiceViewVM extends EntityViewVM { Function(BuildContext) onPaymentsPressed, Function(BuildContext, PaymentEntity) onPaymentPressed, Function(BuildContext) onRefreshed, - Function(BuildContext, String) onUploadDocument, + Function(BuildContext, MultipartFile) onUploadDocument, Function(BuildContext, DocumentEntity, String) onDeleteDocument, Function(BuildContext, DocumentEntity) onViewExpense, }) : super( @@ -106,10 +107,10 @@ class RecurringInvoiceViewVM extends EntityViewVM { onRefreshed: (context) => _handleRefresh(context), onEntityAction: (BuildContext context, EntityAction action) => handleEntitiesActions(context, [invoice], action, autoPop: true), - onUploadDocument: (BuildContext context, String filePath) { + onUploadDocument: (BuildContext context, MultipartFile multipartFile) { final Completer completer = Completer(); store.dispatch(SaveRecurringInvoiceDocumentRequest( - filePath: filePath, invoice: invoice, completer: completer)); + multipartFile: multipartFile, invoice: invoice, completer: completer)); completer.future.then((client) { showToast(AppLocalization.of(context).uploadedDocument); }).catchError((Object error) { diff --git a/lib/ui/settings/company_details.dart b/lib/ui/settings/company_details.dart index 901fa8bf4..49fdcb754 100644 --- a/lib/ui/settings/company_details.dart +++ b/lib/ui/settings/company_details.dart @@ -1,7 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:invoiceninja_flutter/data/models/company_gateway_model.dart'; import 'package:invoiceninja_flutter/data/models/entities.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -21,11 +20,10 @@ import 'package:invoiceninja_flutter/ui/settings/company_details_vm.dart'; import 'package:invoiceninja_flutter/ui/app/edit_scaffold.dart'; import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/dialogs.dart'; +import 'package:invoiceninja_flutter/utils/files.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; -import 'package:invoiceninja_flutter/utils/web_stub.dart' - if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; class CompanyDetails extends StatefulWidget { const CompanyDetails({ @@ -397,8 +395,9 @@ class _CompanyDetailsState extends State ), ), Padding( - padding: const EdgeInsets.all(30), - child: Column( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: ListView( + shrinkWrap: true, children: [ Builder( builder: (context) { @@ -427,20 +426,9 @@ class _CompanyDetailsState extends State label: localization.uploadLogo, iconData: Icons.cloud_upload, onPressed: () async { - String path; - if (kIsWeb) { - path = await WebUtils.filePicker(); - } else { - final image = await ImagePicker().getImage( - source: kReleaseMode - ? ImageSource.gallery - : ImageSource.camera); - if (image != null) { - path = image.path; - } - } - if (path != null) { - viewModel.onUploadLogo(context, path); + final multipartFile = await pickFile(fileIndex: 'company_logo'); + if (multipartFile != null) { + viewModel.onUploadLogo(context, multipartFile); } }, ), @@ -451,7 +439,7 @@ class _CompanyDetailsState extends State ), if ('${settings.companyLogo ?? ''}'.isNotEmpty) Padding( - padding: const EdgeInsets.symmetric(vertical: 30), + padding: const EdgeInsets.symmetric(vertical: 20), child: CachedImage( width: double.infinity, url: settings.companyLogo, diff --git a/lib/ui/settings/company_details_vm.dart b/lib/ui/settings/company_details_vm.dart index f1381038c..02e2d7504 100644 --- a/lib/ui/settings/company_details_vm.dart +++ b/lib/ui/settings/company_details_vm.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/data/models/client_model.dart'; import 'package:invoiceninja_flutter/data/models/company_model.dart'; @@ -112,12 +113,12 @@ class CompanyDetailsVM { break; } }, - onUploadLogo: (context, path) { + onUploadLogo: (context, multipartFile) { final type = state.uiState.settingsUIState.entityType; final completer = snackBarCompleter( context, AppLocalization.of(context).uploadedLogo); store.dispatch( - UploadLogoRequest(completer: completer, path: path, type: type)); + UploadLogoRequest(completer: completer, multipartFile: multipartFile, type: type)); }, onConfigurePaymentTermsPressed: (context) { if (state.paymentTermState.list.isEmpty) { @@ -139,7 +140,7 @@ class CompanyDetailsVM { final Function(SettingsEntity) onSettingsChanged; final Function(CompanyEntity) onCompanyChanged; final Function(BuildContext) onSavePressed; - final Function(BuildContext, String) onUploadLogo; + final Function(BuildContext, MultipartFile) onUploadLogo; final Function(BuildContext) onDeleteLogo; final Function(BuildContext) onConfigurePaymentTermsPressed; } diff --git a/lib/ui/settings/import_export.dart b/lib/ui/settings/import_export.dart index bec2cf3eb..16b6fa69a 100644 --- a/lib/ui/settings/import_export.dart +++ b/lib/ui/settings/import_export.dart @@ -5,6 +5,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/data/models/entities.dart'; import 'package:invoiceninja_flutter/data/models/import_model.dart'; @@ -97,8 +98,7 @@ class _FileImport extends StatefulWidget { class _FileImportState extends State<_FileImport> { var _entityType = EntityType.client; - String _filePath; - String _fileName; + MultipartFile _multipartFile; bool _isLoading = false; void uploadFile() { @@ -113,8 +113,7 @@ class _FileImportState extends State<_FileImport> { webClient.post( url, credentials.token, - filePath: _filePath, - fileIndex: 'file', + multipartFile: _multipartFile, data: { 'entity_type': toSnakeCase('$_entityType'), }, @@ -165,10 +164,10 @@ class _FileImportState extends State<_FileImport> { ), ), DecoratedFormField( - key: ValueKey(_fileName), + key: ValueKey(_multipartFile?.filename), enabled: false, label: localization.csvFile, - initialValue: _fileName ?? localization.noFileSelected, + initialValue: _multipartFile?.filename ?? localization.noFileSelected, ), SizedBox(height: 20), if (_isLoading) @@ -189,11 +188,8 @@ class _FileImportState extends State<_FileImport> { if (result != null) { setState(() { final file = result.files.single; - _filePath = kIsWeb - ? 'data:application/octet-stream;charset=utf-16le;base64,' + - base64Encode(file.bytes) - : file.path; - _fileName = file.name; + _multipartFile = MultipartFile.fromBytes('file', file.bytes, + filename: file.name); }); } }, @@ -205,7 +201,7 @@ class _FileImportState extends State<_FileImport> { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5)), child: Text(localization.uploadFile), - onPressed: _fileName == null ? null : () => uploadFile(), + onPressed: _multipartFile == null ? null : () => uploadFile(), //onPressed: () => uploadFile(), ), ), diff --git a/lib/ui/task/view/task_view_vm.dart b/lib/ui/task/view/task_view_vm.dart index 999da80bc..c46a0549f 100644 --- a/lib/ui/task/view/task_view_vm.dart +++ b/lib/ui/task/view/task_view_vm.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/data/models/task_model.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; @@ -112,10 +113,10 @@ class TaskViewVM { onRefreshed: (context) => _handleRefresh(context), onEntityAction: (BuildContext context, EntityAction action) => handleEntitiesActions(context, [task], action, autoPop: true), - onUploadDocument: (BuildContext context, String filePath) { + onUploadDocument: (BuildContext context, MultipartFile multipartFile) { final Completer completer = Completer(); store.dispatch(SaveTaskDocumentRequest( - filePath: filePath, task: task, completer: completer)); + multipartFile: multipartFile, task: task, completer: completer)); completer.future.then((client) { showToast(AppLocalization.of(context).uploadedDocument); }).catchError((Object error) { @@ -153,6 +154,6 @@ class TaskViewVM { final bool isSaving; final bool isLoading; final bool isDirty; - final Function(BuildContext, String) onUploadDocument; + final Function(BuildContext, MultipartFile) onUploadDocument; final Function(BuildContext, DocumentEntity, String) onDeleteDocument; } diff --git a/lib/ui/vendor/view/vendor_view_vm.dart b/lib/ui/vendor/view/vendor_view_vm.dart index 5de3996e5..e47d54ba3 100644 --- a/lib/ui/vendor/view/vendor_view_vm.dart +++ b/lib/ui/vendor/view/vendor_view_vm.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; +import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/data/models/vendor_model.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; @@ -100,10 +101,10 @@ class VendorViewVM { }, onEntityAction: (BuildContext context, EntityAction action) => handleEntitiesActions(context, [vendor], action, autoPop: true), - onUploadDocument: (BuildContext context, String filePath) { + onUploadDocument: (BuildContext context, MultipartFile multipartFile) { final Completer completer = Completer(); store.dispatch(SaveVendorDocumentRequest( - filePath: filePath, vendor: vendor, completer: completer)); + multipartFile: multipartFile, vendor: vendor, completer: completer)); completer.future.then((client) { showToast(AppLocalization.of(context).uploadedDocument); }).catchError((Object error) { @@ -139,6 +140,6 @@ class VendorViewVM { final bool isSaving; final bool isLoading; final bool isDirty; - final Function(BuildContext, String) onUploadDocument; + final Function(BuildContext, MultipartFile) onUploadDocument; final Function(BuildContext, DocumentEntity, String) onDeleteDocument; } diff --git a/lib/utils/files.dart b/lib/utils/files.dart new file mode 100644 index 000000000..9de405acf --- /dev/null +++ b/lib/utils/files.dart @@ -0,0 +1,31 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; +import 'package:permission_handler/permission_handler.dart'; + +Future pickFile({String fileIndex = 'file'}) async { + if (kIsWeb) { + return _pickFile(fileIndex: fileIndex); + } else { + final permissionStatus = await [Permission.photos].request(); + final permission = + permissionStatus[Permission.photos] ?? PermissionStatus.undetermined; + if (permission == PermissionStatus.granted) { + return _pickFile(fileIndex: fileIndex); + } else { + openAppSettings(); + return null; + } + } +} + +Future _pickFile({String fileIndex}) async { + final result = await FilePicker.platform.pickFiles(); + if (result != null) { + final file = result.files.single; + return MultipartFile.fromBytes(fileIndex ?? 'file', file.bytes, + filename: file.name); + } + + return null; +} diff --git a/lib/utils/web.dart b/lib/utils/web.dart index 20c0946ca..f7a47a8ad 100644 --- a/lib/utils/web.dart +++ b/lib/utils/web.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:convert'; import 'dart:html'; import 'dart:typed_data'; @@ -14,24 +13,6 @@ class WebUtils { static String getHtmlValue(String field) => window.document.documentElement.dataset[field]; - static Future filePicker() { - final completer = new Completer(); - final InputElement input = document.createElement('input'); - input - ..type = 'file' - ..accept = 'image/*'; - input.onChange.listen((e) async { - final List files = input.files; - final reader = new FileReader(); - reader.readAsDataUrl(files[0]); - reader.onError.listen((error) => completer.completeError(error)); - await reader.onLoad.first; - completer.complete(reader.result as String); - }); - input.click(); - return completer.future; - } - static void downloadTextFile(String filename, String data) { final encodedFileContents = Uri.encodeComponent(data); AnchorElement(href: 'data:text/plain;charset=utf-8,$encodedFileContents') diff --git a/lib/utils/web_stub.dart b/lib/utils/web_stub.dart index 1c0526cab..8817e97d4 100644 --- a/lib/utils/web_stub.dart +++ b/lib/utils/web_stub.dart @@ -8,8 +8,6 @@ class WebUtils { static String getHtmlValue(String field) => null; - static Future filePicker() => null; - static void downloadTextFile(String filename, String data) {} static void downloadBinaryFile(String filename, Uint8List data) {}