feat: add Flutter web build support
This commit is contained in:
@@ -1,146 +1,2 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class EncryptedDraftStore {
|
||||
EncryptedDraftStore({
|
||||
FlutterSecureStorage? storage,
|
||||
AesGcm? algorithm,
|
||||
}) : _storage = storage ?? const FlutterSecureStorage(),
|
||||
_algorithm = algorithm ?? AesGcm.with256bits();
|
||||
|
||||
static const _keyPrefix = 'service_draft_key_';
|
||||
final FlutterSecureStorage _storage;
|
||||
final AesGcm _algorithm;
|
||||
|
||||
Future<void> saveDraft({
|
||||
required String accountIdentity,
|
||||
required String taskIdentity,
|
||||
required Map<String, Object?> value,
|
||||
}) async {
|
||||
final directory = await _accountDirectory(accountIdentity);
|
||||
final key = await _key(accountIdentity);
|
||||
final nonce = List<int>.generate(12, (_) => Random.secure().nextInt(256));
|
||||
final box = await _algorithm.encrypt(
|
||||
utf8.encode(jsonEncode(value)),
|
||||
secretKey: key,
|
||||
nonce: nonce,
|
||||
);
|
||||
final payload = jsonEncode({
|
||||
'nonce': base64Encode(box.nonce),
|
||||
'cipherText': base64Encode(box.cipherText),
|
||||
'mac': base64Encode(box.mac.bytes),
|
||||
});
|
||||
await File('${directory.path}/$taskIdentity.draft').writeAsString(payload, flush: true);
|
||||
}
|
||||
|
||||
Future<String> sealAttachment({
|
||||
required String accountIdentity,
|
||||
required String taskIdentity,
|
||||
required String stage,
|
||||
required String sourcePath,
|
||||
}) async {
|
||||
final source = File(sourcePath);
|
||||
final bytes = await source.readAsBytes();
|
||||
final box = await _encrypt(accountIdentity, bytes);
|
||||
final name = '$taskIdentity-$stage-${DateTime.now().microsecondsSinceEpoch}.evidence';
|
||||
final file = File('${(await _accountDirectory(accountIdentity)).path}/$name');
|
||||
await file.writeAsString(_boxJson(box), flush: true);
|
||||
if (source.existsSync()) await source.delete();
|
||||
return name;
|
||||
}
|
||||
|
||||
Future<String> materializeAttachment({
|
||||
required String accountIdentity,
|
||||
required String sealedName,
|
||||
}) async {
|
||||
final source = File('${(await _accountDirectory(accountIdentity)).path}/$sealedName');
|
||||
final clear = await _decrypt(accountIdentity, await source.readAsString());
|
||||
final temporary = await getTemporaryDirectory();
|
||||
final file = File('${temporary.path}/${sealedName.replaceAll('.evidence', '.jpg')}');
|
||||
await file.writeAsBytes(clear, flush: true);
|
||||
return file.path;
|
||||
}
|
||||
|
||||
Future<Map<String, Object?>?> readDraft(String accountIdentity, String taskIdentity) async {
|
||||
final file = File('${(await _accountDirectory(accountIdentity)).path}/$taskIdentity.draft');
|
||||
if (!file.existsSync()) return null;
|
||||
final payload = jsonDecode(await file.readAsString());
|
||||
if (payload is! Map) return null;
|
||||
final clear = await _decrypt(accountIdentity, await file.readAsString());
|
||||
final decoded = jsonDecode(utf8.decode(clear));
|
||||
if (decoded is! Map) return null;
|
||||
return decoded.map<String, Object?>((key, value) => MapEntry(key.toString(), value));
|
||||
}
|
||||
|
||||
Future<int> count(String accountIdentity) async {
|
||||
final directory = await _accountDirectory(accountIdentity);
|
||||
return directory
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.where((file) => file.path.endsWith('.draft'))
|
||||
.length;
|
||||
}
|
||||
|
||||
Future<void> deleteDraft(String accountIdentity, String taskIdentity) async {
|
||||
final directory = await _accountDirectory(accountIdentity);
|
||||
final file = File('${directory.path}/$taskIdentity.draft');
|
||||
if (file.existsSync()) await file.delete();
|
||||
for (final evidence in directory.listSync().whereType<File>().where(
|
||||
(item) => item.uri.pathSegments.last.startsWith('$taskIdentity-'),
|
||||
)) {
|
||||
await evidence.delete();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> discardAccount(String accountIdentity) async {
|
||||
final directory = await _accountDirectory(accountIdentity);
|
||||
if (directory.existsSync()) await directory.delete(recursive: true);
|
||||
await _storage.delete(key: '$_keyPrefix$accountIdentity');
|
||||
}
|
||||
|
||||
Future<Directory> _accountDirectory(String accountIdentity) async {
|
||||
final root = await getApplicationSupportDirectory();
|
||||
final directory = Directory('${root.path}/drafts/$accountIdentity');
|
||||
if (!directory.existsSync()) await directory.create(recursive: true);
|
||||
return directory;
|
||||
}
|
||||
|
||||
Future<SecretKey> _key(String accountIdentity) async {
|
||||
final storageKey = '$_keyPrefix$accountIdentity';
|
||||
var encoded = await _storage.read(key: storageKey);
|
||||
if (encoded == null) {
|
||||
encoded = base64Encode(await (await _algorithm.newSecretKey()).extractBytes());
|
||||
await _storage.write(key: storageKey, value: encoded);
|
||||
}
|
||||
return SecretKey(base64Decode(encoded));
|
||||
}
|
||||
|
||||
Future<SecretBox> _encrypt(String accountIdentity, List<int> clear) async {
|
||||
final nonce = List<int>.generate(12, (_) => Random.secure().nextInt(256));
|
||||
return _algorithm.encrypt(clear, secretKey: await _key(accountIdentity), nonce: nonce);
|
||||
}
|
||||
|
||||
Future<List<int>> _decrypt(String accountIdentity, String encodedPayload) async {
|
||||
final payload = jsonDecode(encodedPayload);
|
||||
if (payload is! Map) throw const FormatException('Invalid encrypted draft');
|
||||
return _algorithm.decrypt(
|
||||
SecretBox(
|
||||
base64Decode(payload['cipherText'] as String),
|
||||
nonce: base64Decode(payload['nonce'] as String),
|
||||
mac: Mac(base64Decode(payload['mac'] as String)),
|
||||
),
|
||||
secretKey: await _key(accountIdentity),
|
||||
);
|
||||
}
|
||||
|
||||
String _boxJson(SecretBox box) => jsonEncode({
|
||||
'nonce': base64Encode(box.nonce),
|
||||
'cipherText': base64Encode(box.cipherText),
|
||||
'mac': base64Encode(box.mac.bytes),
|
||||
});
|
||||
}
|
||||
export 'encrypted_draft_store_io.dart'
|
||||
if (dart.library.html) 'encrypted_draft_store_web.dart';
|
||||
|
||||
154
apps/service_app/lib/data/offline/encrypted_draft_store_io.dart
Normal file
154
apps/service_app/lib/data/offline/encrypted_draft_store_io.dart
Normal file
@@ -0,0 +1,154 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
/// Android/iOS 端按账号加密保存现场草稿和附件。
|
||||
class EncryptedDraftStore {
|
||||
EncryptedDraftStore({FlutterSecureStorage? storage, AesGcm? algorithm})
|
||||
: _storage = storage ?? const FlutterSecureStorage(),
|
||||
_algorithm = algorithm ?? AesGcm.with256bits();
|
||||
|
||||
static const _keyPrefix = 'service_draft_key_';
|
||||
final FlutterSecureStorage _storage;
|
||||
final AesGcm _algorithm;
|
||||
|
||||
Future<void> saveDraft({
|
||||
required String accountIdentity,
|
||||
required String taskIdentity,
|
||||
required Map<String, Object?> value,
|
||||
}) async {
|
||||
final directory = await _accountDirectory(accountIdentity);
|
||||
final box = await _encrypt(accountIdentity, utf8.encode(jsonEncode(value)));
|
||||
await File(
|
||||
'${directory.path}/$taskIdentity.draft',
|
||||
).writeAsString(_boxJson(box), flush: true);
|
||||
}
|
||||
|
||||
Future<String> sealAttachment({
|
||||
required String accountIdentity,
|
||||
required String taskIdentity,
|
||||
required String stage,
|
||||
required XFile source,
|
||||
}) async {
|
||||
final box = await _encrypt(accountIdentity, await source.readAsBytes());
|
||||
final name = '$taskIdentity-$stage-${DateTime.now().microsecondsSinceEpoch}.evidence';
|
||||
final file = File(
|
||||
'${(await _accountDirectory(accountIdentity)).path}/$name',
|
||||
);
|
||||
await file.writeAsString(_boxJson(box), flush: true);
|
||||
final sourceFile = File(source.path);
|
||||
if (sourceFile.existsSync()) await sourceFile.delete();
|
||||
return name;
|
||||
}
|
||||
|
||||
Future<XFile> materializeAttachment({
|
||||
required String accountIdentity,
|
||||
required String sealedName,
|
||||
}) async {
|
||||
final source = File(
|
||||
'${(await _accountDirectory(accountIdentity)).path}/$sealedName',
|
||||
);
|
||||
final clear = await _decrypt(accountIdentity, await source.readAsString());
|
||||
final temporary = await getTemporaryDirectory();
|
||||
final file = File(
|
||||
'${temporary.path}/${sealedName.replaceAll('.evidence', '.jpg')}',
|
||||
);
|
||||
await file.writeAsBytes(clear, flush: true);
|
||||
return XFile(file.path);
|
||||
}
|
||||
|
||||
Future<void> discardMaterializedAttachment(XFile file) async {
|
||||
final temporary = File(file.path);
|
||||
if (temporary.existsSync()) await temporary.delete();
|
||||
}
|
||||
|
||||
Future<Map<String, Object?>?> readDraft(
|
||||
String accountIdentity,
|
||||
String taskIdentity,
|
||||
) async {
|
||||
final file = File(
|
||||
'${(await _accountDirectory(accountIdentity)).path}/$taskIdentity.draft',
|
||||
);
|
||||
if (!file.existsSync()) return null;
|
||||
final decoded = jsonDecode(
|
||||
utf8.decode(await _decrypt(accountIdentity, await file.readAsString())),
|
||||
);
|
||||
if (decoded is! Map) return null;
|
||||
return decoded.map<String, Object?>(
|
||||
(key, value) => MapEntry(key.toString(), value),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> count(String accountIdentity) async => (await _accountDirectory(
|
||||
accountIdentity,
|
||||
)).listSync().whereType<File>().where((file) => file.path.endsWith('.draft')).length;
|
||||
|
||||
Future<void> deleteDraft(String accountIdentity, String taskIdentity) async {
|
||||
final directory = await _accountDirectory(accountIdentity);
|
||||
final file = File('${directory.path}/$taskIdentity.draft');
|
||||
if (file.existsSync()) await file.delete();
|
||||
for (final evidence in directory.listSync().whereType<File>().where(
|
||||
(item) => item.uri.pathSegments.last.startsWith('$taskIdentity-'),
|
||||
)) {
|
||||
await evidence.delete();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> discardAccount(String accountIdentity) async {
|
||||
final directory = await _accountDirectory(accountIdentity);
|
||||
if (directory.existsSync()) await directory.delete(recursive: true);
|
||||
await _storage.delete(key: '$_keyPrefix$accountIdentity');
|
||||
}
|
||||
|
||||
Future<Directory> _accountDirectory(String accountIdentity) async {
|
||||
final root = await getApplicationSupportDirectory();
|
||||
final directory = Directory('${root.path}/drafts/$accountIdentity');
|
||||
if (!directory.existsSync()) await directory.create(recursive: true);
|
||||
return directory;
|
||||
}
|
||||
|
||||
Future<SecretKey> _key(String accountIdentity) async {
|
||||
final storageKey = '$_keyPrefix$accountIdentity';
|
||||
var encoded = await _storage.read(key: storageKey);
|
||||
if (encoded == null) {
|
||||
encoded = base64Encode(
|
||||
await (await _algorithm.newSecretKey()).extractBytes(),
|
||||
);
|
||||
await _storage.write(key: storageKey, value: encoded);
|
||||
}
|
||||
return SecretKey(base64Decode(encoded));
|
||||
}
|
||||
|
||||
Future<SecretBox> _encrypt(String accountIdentity, List<int> clear) async => _algorithm.encrypt(
|
||||
clear,
|
||||
secretKey: await _key(accountIdentity),
|
||||
nonce: List<int>.generate(12, (_) => Random.secure().nextInt(256)),
|
||||
);
|
||||
|
||||
Future<List<int>> _decrypt(
|
||||
String accountIdentity,
|
||||
String encodedPayload,
|
||||
) async {
|
||||
final payload = jsonDecode(encodedPayload);
|
||||
if (payload is! Map) throw const FormatException('Invalid encrypted draft');
|
||||
return _algorithm.decrypt(
|
||||
SecretBox(
|
||||
base64Decode(payload['cipherText'] as String),
|
||||
nonce: base64Decode(payload['nonce'] as String),
|
||||
mac: Mac(base64Decode(payload['mac'] as String)),
|
||||
),
|
||||
secretKey: await _key(accountIdentity),
|
||||
);
|
||||
}
|
||||
|
||||
String _boxJson(SecretBox box) => jsonEncode({
|
||||
'nonce': base64Encode(box.nonce),
|
||||
'cipherText': base64Encode(box.cipherText),
|
||||
'mac': base64Encode(box.mac.bytes),
|
||||
});
|
||||
}
|
||||
150
apps/service_app/lib/data/offline/encrypted_draft_store_web.dart
Normal file
150
apps/service_app/lib/data/offline/encrypted_draft_store_web.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:web/web.dart' as web;
|
||||
|
||||
/// Web 端仅保存 AES-GCM 密文;密钥继续由浏览器安全存储插件管理。
|
||||
class EncryptedDraftStore {
|
||||
EncryptedDraftStore({FlutterSecureStorage? storage, AesGcm? algorithm})
|
||||
: _storage = storage ?? const FlutterSecureStorage(),
|
||||
_algorithm = algorithm ?? AesGcm.with256bits();
|
||||
|
||||
static const _keyPrefix = 'service_draft_key_';
|
||||
static const _entryPrefix = 'service_draft_entry_';
|
||||
final FlutterSecureStorage _storage;
|
||||
final AesGcm _algorithm;
|
||||
|
||||
Future<void> saveDraft({
|
||||
required String accountIdentity,
|
||||
required String taskIdentity,
|
||||
required Map<String, Object?> value,
|
||||
}) async {
|
||||
final box = await _encrypt(accountIdentity, utf8.encode(jsonEncode(value)));
|
||||
_entries.setItem(_draftKey(accountIdentity, taskIdentity), _boxJson(box));
|
||||
}
|
||||
|
||||
Future<String> sealAttachment({
|
||||
required String accountIdentity,
|
||||
required String taskIdentity,
|
||||
required String stage,
|
||||
required XFile source,
|
||||
}) async {
|
||||
final box = await _encrypt(accountIdentity, await source.readAsBytes());
|
||||
final name = '$taskIdentity-$stage-${DateTime.now().microsecondsSinceEpoch}.evidence';
|
||||
_entries.setItem(
|
||||
_attachmentKey(accountIdentity, name),
|
||||
jsonEncode({'box': _boxJson(box), 'file_name': source.name}),
|
||||
);
|
||||
return name;
|
||||
}
|
||||
|
||||
Future<XFile> materializeAttachment({
|
||||
required String accountIdentity,
|
||||
required String sealedName,
|
||||
}) async {
|
||||
final encoded = _entries.getItem(_attachmentKey(accountIdentity, sealedName));
|
||||
if (encoded == null) {
|
||||
throw StateError('未找到加密附件');
|
||||
}
|
||||
final payload = jsonDecode(encoded);
|
||||
if (payload is! Map) {
|
||||
throw const FormatException('Invalid encrypted attachment');
|
||||
}
|
||||
final clear = await _decrypt(accountIdentity, payload['box'] as String);
|
||||
return XFile.fromData(
|
||||
Uint8List.fromList(clear),
|
||||
name: payload['file_name'] as String? ?? '$sealedName.jpg',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> discardMaterializedAttachment(XFile file) async {}
|
||||
|
||||
Future<Map<String, Object?>?> readDraft(String accountIdentity, String taskIdentity) async {
|
||||
final encoded = _entries.getItem(_draftKey(accountIdentity, taskIdentity));
|
||||
if (encoded == null) {
|
||||
return null;
|
||||
}
|
||||
final decoded = jsonDecode(utf8.decode(await _decrypt(accountIdentity, encoded)));
|
||||
if (decoded is! Map) {
|
||||
return null;
|
||||
}
|
||||
return decoded.map<String, Object?>((key, value) => MapEntry(key.toString(), value));
|
||||
}
|
||||
|
||||
Future<int> count(String accountIdentity) async =>
|
||||
_entryKeys.where((key) => key.startsWith('$_entryPrefix${accountIdentity}_draft_')).length;
|
||||
|
||||
Future<void> deleteDraft(String accountIdentity, String taskIdentity) async {
|
||||
final prefix = '$_entryPrefix${accountIdentity}_';
|
||||
_entries.removeItem(
|
||||
'$prefix'
|
||||
'draft_$taskIdentity',
|
||||
);
|
||||
final attachmentPrefix =
|
||||
'$prefix'
|
||||
'attachment_$taskIdentity-';
|
||||
for (final key in _entryKeys.where((key) => key.startsWith(attachmentPrefix)).toList()) {
|
||||
_entries.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> discardAccount(String accountIdentity) async {
|
||||
final prefix = '$_entryPrefix${accountIdentity}_';
|
||||
for (final key in _entryKeys.where((key) => key.startsWith(prefix)).toList()) {
|
||||
_entries.removeItem(key);
|
||||
}
|
||||
await _storage.delete(key: '$_keyPrefix$accountIdentity');
|
||||
}
|
||||
|
||||
web.Storage get _entries => web.window.localStorage;
|
||||
|
||||
Iterable<String> get _entryKeys =>
|
||||
List<String?>.generate(_entries.length, (index) => _entries.key(index)).whereType<String>();
|
||||
|
||||
String _draftKey(String accountIdentity, String taskIdentity) =>
|
||||
'$_entryPrefix${accountIdentity}_draft_$taskIdentity';
|
||||
|
||||
String _attachmentKey(String accountIdentity, String sealedName) =>
|
||||
'$_entryPrefix${accountIdentity}_attachment_$sealedName';
|
||||
|
||||
Future<SecretKey> _key(String accountIdentity) async {
|
||||
final storageKey = '$_keyPrefix$accountIdentity';
|
||||
var encoded = await _storage.read(key: storageKey);
|
||||
if (encoded == null) {
|
||||
encoded = base64Encode(await (await _algorithm.newSecretKey()).extractBytes());
|
||||
await _storage.write(key: storageKey, value: encoded);
|
||||
}
|
||||
return SecretKey(base64Decode(encoded));
|
||||
}
|
||||
|
||||
Future<SecretBox> _encrypt(String accountIdentity, List<int> clear) async => _algorithm.encrypt(
|
||||
clear,
|
||||
secretKey: await _key(accountIdentity),
|
||||
nonce: List<int>.generate(12, (_) => Random.secure().nextInt(256)),
|
||||
);
|
||||
|
||||
Future<List<int>> _decrypt(String accountIdentity, String encodedPayload) async {
|
||||
final payload = jsonDecode(encodedPayload);
|
||||
if (payload is! Map) {
|
||||
throw const FormatException('Invalid encrypted draft');
|
||||
}
|
||||
return _algorithm.decrypt(
|
||||
SecretBox(
|
||||
base64Decode(payload['cipherText'] as String),
|
||||
nonce: base64Decode(payload['nonce'] as String),
|
||||
mac: Mac(base64Decode(payload['mac'] as String)),
|
||||
),
|
||||
secretKey: await _key(accountIdentity),
|
||||
);
|
||||
}
|
||||
|
||||
String _boxJson(SecretBox box) => jsonEncode({
|
||||
'nonce': base64Encode(box.nonce),
|
||||
'cipherText': base64Encode(box.cipherText),
|
||||
'mac': base64Encode(box.mac.bytes),
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import '../../domain/models/service_models.dart';
|
||||
import '../services/api_client.dart';
|
||||
@@ -86,14 +87,17 @@ class ServiceRepository {
|
||||
|
||||
Future<void> arrive(String identity) async {
|
||||
final point = await _location.current();
|
||||
await _api.post('$root/delivery/orders/$identity/arrive', body: _pointJson(point));
|
||||
await _api.post(
|
||||
'$root/delivery/orders/$identity/arrive',
|
||||
body: _pointJson(point),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> submitDeliveryReceipt({
|
||||
required String identity,
|
||||
required String recipientName,
|
||||
required String recipientPhone,
|
||||
required String proofFile,
|
||||
required XFile proofFile,
|
||||
}) async {
|
||||
final proofUri = await _api.upload(proofFile);
|
||||
await _api.post(
|
||||
@@ -118,7 +122,7 @@ class ServiceRepository {
|
||||
final uploaded = <Map<String, Object?>>[];
|
||||
for (final item in evidence) {
|
||||
final uri = await _api.upload(
|
||||
item.filePath,
|
||||
item.file,
|
||||
contentType: item.mediaType == 'video' ? 'video/mp4' : 'image/jpeg',
|
||||
);
|
||||
uploaded.add({
|
||||
@@ -153,14 +157,14 @@ class EvidenceInput {
|
||||
const EvidenceInput({
|
||||
required this.evidenceType,
|
||||
required this.mediaType,
|
||||
required this.filePath,
|
||||
required this.file,
|
||||
required this.capturedAt,
|
||||
required this.requestNo,
|
||||
});
|
||||
|
||||
final String evidenceType;
|
||||
final String mediaType;
|
||||
final String filePath;
|
||||
final XFile file;
|
||||
final DateTime capturedAt;
|
||||
final String requestNo;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
class ApiException implements Exception {
|
||||
const ApiException(this.code, this.message);
|
||||
@@ -38,11 +38,21 @@ class ApiClient {
|
||||
|
||||
Future<Object?> put(String path, {Map<String, Object?>? body}) => _send('PUT', path, body: body);
|
||||
|
||||
Future<String> upload(String filePath, {String contentType = 'image/jpeg'}) async {
|
||||
final request = http.MultipartRequest('POST', Uri.parse('$baseUrl/upload/file'));
|
||||
request.headers[HttpHeaders.authorizationHeader] = _tokenProvider();
|
||||
/// 上传跨平台文件对象;浏览器端不能依赖本地文件路径。
|
||||
Future<String> upload(XFile file, {String contentType = 'image/jpeg'}) async {
|
||||
final request = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse('$baseUrl/upload/file'),
|
||||
);
|
||||
request.headers['authorization'] = _tokenProvider();
|
||||
request.fields['declared_content_type'] = contentType;
|
||||
request.files.add(await http.MultipartFile.fromPath('file', filePath));
|
||||
request.files.add(
|
||||
http.MultipartFile.fromBytes(
|
||||
'file',
|
||||
await file.readAsBytes(),
|
||||
filename: file.name,
|
||||
),
|
||||
);
|
||||
final response = await http.Response.fromStream(await request.send());
|
||||
final details = _decode(response);
|
||||
return jsonMap(details)['uri'] as String? ?? '';
|
||||
@@ -55,15 +65,17 @@ class ApiClient {
|
||||
bool authenticated = true,
|
||||
}) async {
|
||||
final request = http.Request(method, Uri.parse('$baseUrl$path'));
|
||||
request.headers[HttpHeaders.acceptHeader] = 'application/json';
|
||||
request.headers['accept'] = 'application/json';
|
||||
if (authenticated && _tokenProvider().isNotEmpty) {
|
||||
request.headers[HttpHeaders.authorizationHeader] = _tokenProvider();
|
||||
request.headers['authorization'] = _tokenProvider();
|
||||
}
|
||||
if (body != null) {
|
||||
request.headers[HttpHeaders.contentTypeHeader] = 'application/json; charset=UTF-8';
|
||||
request.headers['content-type'] = 'application/json; charset=UTF-8';
|
||||
request.body = jsonEncode(body);
|
||||
}
|
||||
final response = await http.Response.fromStream(await _client.send(request));
|
||||
final response = await http.Response.fromStream(
|
||||
await _client.send(request),
|
||||
);
|
||||
return _decode(response);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user