✨ Attachment can link exists things
♿ Optimize upload progress
This commit is contained in:
parent
98cc313a91
commit
11fb79623e
@ -180,14 +180,13 @@ class AttachmentUploaderController extends GetxController {
|
|||||||
{Function(double)? onProgress}) async {
|
{Function(double)? onProgress}) async {
|
||||||
final AttachmentProvider provider = Get.find();
|
final AttachmentProvider provider = Get.find();
|
||||||
try {
|
try {
|
||||||
final resp = await provider.createAttachment(
|
final result = await provider.createAttachment(
|
||||||
data,
|
data,
|
||||||
path,
|
path,
|
||||||
usage,
|
usage,
|
||||||
metadata,
|
metadata,
|
||||||
onProgress: onProgress,
|
onProgress: onProgress,
|
||||||
);
|
);
|
||||||
var result = Attachment.fromJson(resp.body);
|
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
rethrow;
|
rethrow;
|
||||||
|
@ -6,6 +6,7 @@ import 'package:path/path.dart';
|
|||||||
import 'package:solian/models/attachment.dart';
|
import 'package:solian/models/attachment.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
|
import 'package:dio/dio.dart' as dio;
|
||||||
|
|
||||||
class AttachmentProvider extends GetConnect {
|
class AttachmentProvider extends GetConnect {
|
||||||
static Map<String, String> mimetypeOverrides = {
|
static Map<String, String> mimetypeOverrides = {
|
||||||
@ -37,18 +38,14 @@ class AttachmentProvider extends GetConnect {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> createAttachment(
|
Future<Attachment> createAttachment(
|
||||||
Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
|
Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
|
||||||
{Function(double)? onProgress}) async {
|
{Function(double)? onProgress}) async {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||||
|
|
||||||
final client = auth.configureClient(
|
final filePayload =
|
||||||
'files',
|
dio.MultipartFile.fromBytes(data, filename: basename(path));
|
||||||
timeout: const Duration(minutes: 3),
|
|
||||||
);
|
|
||||||
|
|
||||||
final filePayload = MultipartFile(data, filename: basename(path));
|
|
||||||
final fileAlt = basename(path).contains('.')
|
final fileAlt = basename(path).contains('.')
|
||||||
? basename(path).substring(0, basename(path).lastIndexOf('.'))
|
? basename(path).substring(0, basename(path).lastIndexOf('.'))
|
||||||
: basename(path);
|
: basename(path);
|
||||||
@ -61,25 +58,31 @@ class AttachmentProvider extends GetConnect {
|
|||||||
if (mimetypeOverrides.keys.contains(fileExt)) {
|
if (mimetypeOverrides.keys.contains(fileExt)) {
|
||||||
mimetypeOverride = mimetypeOverrides[fileExt];
|
mimetypeOverride = mimetypeOverrides[fileExt];
|
||||||
}
|
}
|
||||||
final payload = FormData({
|
final payload = dio.FormData.fromMap({
|
||||||
'alt': fileAlt,
|
'alt': fileAlt,
|
||||||
'file': filePayload,
|
'file': filePayload,
|
||||||
'usage': usage,
|
'usage': usage,
|
||||||
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
|
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
|
||||||
'metadata': jsonEncode(metadata),
|
'metadata': jsonEncode(metadata),
|
||||||
});
|
});
|
||||||
final resp = await client.post(
|
final resp = await dio.Dio(
|
||||||
|
dio.BaseOptions(
|
||||||
|
baseUrl: ServiceFinder.buildUrl('files', null),
|
||||||
|
headers: {'Authorization': 'Bearer ${auth.credentials!.accessToken}'},
|
||||||
|
),
|
||||||
|
).post(
|
||||||
'/attachments',
|
'/attachments',
|
||||||
payload,
|
data: payload,
|
||||||
uploadProgress: (progress) {
|
onSendProgress: (count, total) {
|
||||||
if (onProgress != null) onProgress(progress);
|
if (onProgress != null) onProgress(count / total);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
throw Exception(resp.bodyString);
|
print(resp.data);
|
||||||
|
throw Exception(resp.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp;
|
return Attachment.fromJson(resp.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> updateAttachment(
|
Future<Response> updateAttachment(
|
||||||
|
@ -7,6 +7,7 @@ import 'package:image_cropper/image_cropper.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
|
import 'package:solian/models/attachment.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/attachment.dart';
|
import 'package:solian/providers/content/attachment.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
@ -104,9 +105,9 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
|
|
||||||
final AttachmentProvider provider = Get.find();
|
final AttachmentProvider provider = Get.find();
|
||||||
|
|
||||||
Response? attachResp;
|
Attachment? attachResult;
|
||||||
try {
|
try {
|
||||||
attachResp = await provider.createAttachment(
|
attachResult = await provider.createAttachment(
|
||||||
await file.readAsBytes(),
|
await file.readAsBytes(),
|
||||||
file.path,
|
file.path,
|
||||||
'p.$position',
|
'p.$position',
|
||||||
@ -122,7 +123,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
|
|
||||||
final resp = await client.put(
|
final resp = await client.put(
|
||||||
'/users/me/$position',
|
'/users/me/$position',
|
||||||
{'attachment': attachResp.body['id']},
|
{'attachment': attachResult.id},
|
||||||
);
|
);
|
||||||
if (resp.statusCode == 200) {
|
if (resp.statusCode == 200) {
|
||||||
_syncWidget();
|
_syncWidget();
|
||||||
|
@ -13,6 +13,7 @@ const i18nEnglish = {
|
|||||||
'more': 'More',
|
'more': 'More',
|
||||||
'share': 'Share',
|
'share': 'Share',
|
||||||
'feed': 'Feed',
|
'feed': 'Feed',
|
||||||
|
'unlink': 'Unlink',
|
||||||
'feedSearch': 'Search Feed',
|
'feedSearch': 'Search Feed',
|
||||||
'feedSearchWithTag': 'Searching with tag #@key',
|
'feedSearchWithTag': 'Searching with tag #@key',
|
||||||
'feedSearchWithCategory': 'Searching in category @category',
|
'feedSearchWithCategory': 'Searching in category @category',
|
||||||
@ -171,6 +172,9 @@ const i18nEnglish = {
|
|||||||
'attachmentAddCameraVideo': 'Capture video',
|
'attachmentAddCameraVideo': 'Capture video',
|
||||||
'attachmentAddClipboard': 'Paste file',
|
'attachmentAddClipboard': 'Paste file',
|
||||||
'attachmentAddFile': 'Attach file',
|
'attachmentAddFile': 'Attach file',
|
||||||
|
'attachmentAddLink': 'Link attachments',
|
||||||
|
'attachmentAddLinkHint': 'Enter attachment serial number to link that attachment',
|
||||||
|
'attachmentAddLinkInput': 'Serial number',
|
||||||
'attachmentSetting': 'Adjust attachment',
|
'attachmentSetting': 'Adjust attachment',
|
||||||
'attachmentAlt': 'Alternative text',
|
'attachmentAlt': 'Alternative text',
|
||||||
'attachmentLoadFailed': 'Load Attachment Failed',
|
'attachmentLoadFailed': 'Load Attachment Failed',
|
||||||
|
@ -21,6 +21,7 @@ const i18nSimplifiedChinese = {
|
|||||||
'more': '更多',
|
'more': '更多',
|
||||||
'share': '分享',
|
'share': '分享',
|
||||||
'feed': '资讯',
|
'feed': '资讯',
|
||||||
|
'unlink': '移除链接',
|
||||||
'feedSearch': '搜索资讯',
|
'feedSearch': '搜索资讯',
|
||||||
'feedSearchWithTag': '检索带有 #@key 标签的资讯',
|
'feedSearchWithTag': '检索带有 #@key 标签的资讯',
|
||||||
'feedSearchWithCategory': '检索位于分类 @category 的资讯',
|
'feedSearchWithCategory': '检索位于分类 @category 的资讯',
|
||||||
@ -160,6 +161,9 @@ const i18nSimplifiedChinese = {
|
|||||||
'attachmentAddCameraVideo': '拍摄视频',
|
'attachmentAddCameraVideo': '拍摄视频',
|
||||||
'attachmentAddClipboard': '粘贴文件',
|
'attachmentAddClipboard': '粘贴文件',
|
||||||
'attachmentAddFile': '附加文件',
|
'attachmentAddFile': '附加文件',
|
||||||
|
'attachmentAddLink': '链接附件',
|
||||||
|
'attachmentAddLinkHint': '输入附件的神秘代号来链接对应附件',
|
||||||
|
'attachmentAddLinkInput': '神秘代号',
|
||||||
'attachmentSetting': '调整附件',
|
'attachmentSetting': '调整附件',
|
||||||
'attachmentAlt': '替代文字',
|
'attachmentAlt': '替代文字',
|
||||||
'attachmentLoadFailed': '加载失败',
|
'attachmentLoadFailed': '加载失败',
|
||||||
|
@ -110,6 +110,63 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _linkAttachments() async {
|
||||||
|
final controller = TextEditingController();
|
||||||
|
final input = await showDialog<String?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('attachmentAddLink'.tr),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('attachmentAddLinkHint'.tr, textAlign: TextAlign.left),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'attachmentAddLinkInput'.tr,
|
||||||
|
),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor:
|
||||||
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text('cancel'.tr),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: Text('next'.tr),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, controller.text);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => controller.dispose());
|
||||||
|
|
||||||
|
if (input == null || input.isEmpty) return;
|
||||||
|
final value = int.tryParse(input);
|
||||||
|
if (value == null) return;
|
||||||
|
|
||||||
|
final AttachmentProvider attach = Get.find();
|
||||||
|
final result = await attach.getMetadata(value);
|
||||||
|
if (result != null) {
|
||||||
|
widget.onAdd(result.id);
|
||||||
|
setState(() => _attachments.add(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _pasteFileToUpload() async {
|
void _pasteFileToUpload() async {
|
||||||
final data = await Pasteboard.image;
|
final data = await Pasteboard.image;
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
@ -150,7 +207,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _revertMetadataList() {
|
void _revertMetadataList() {
|
||||||
final AttachmentProvider provider = Get.find();
|
final AttachmentProvider attach = Get.find();
|
||||||
|
|
||||||
if (widget.initialAttachments.isEmpty) {
|
if (widget.initialAttachments.isEmpty) {
|
||||||
_isFirstTimeBusy = false;
|
_isFirstTimeBusy = false;
|
||||||
@ -167,7 +224,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
|
|
||||||
int progress = 0;
|
int progress = 0;
|
||||||
for (var idx = 0; idx < widget.initialAttachments.length; idx++) {
|
for (var idx = 0; idx < widget.initialAttachments.length; idx++) {
|
||||||
provider.getMetadata(widget.initialAttachments[idx]).then((resp) {
|
attach.getMetadata(widget.initialAttachments[idx]).then((resp) {
|
||||||
progress++;
|
progress++;
|
||||||
_attachments[idx] = resp;
|
_attachments[idx] = resp;
|
||||||
if (progress == widget.initialAttachments.length) {
|
if (progress == widget.initialAttachments.length) {
|
||||||
@ -425,6 +482,19 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text('unlink'.tr),
|
||||||
|
leading: const Icon(Icons.link_off),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
widget.onRemove(element.id);
|
||||||
|
setState(() => _attachments.removeAt(index));
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -661,6 +731,12 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
style: const ButtonStyle(visualDensity: density),
|
style: const ButtonStyle(visualDensity: density),
|
||||||
onPressed: () => _pickFileToUpload(),
|
onPressed: () => _pickFileToUpload(),
|
||||||
),
|
),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.link),
|
||||||
|
label: Text('attachmentAddFile'.tr),
|
||||||
|
style: const ButtonStyle(visualDensity: density),
|
||||||
|
onPressed: () => _linkAttachments(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
).paddingSymmetric(horizontal: 12),
|
).paddingSymmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
|
@ -24,7 +24,7 @@ class ChatEventList extends StatelessWidget {
|
|||||||
required this.onReply,
|
required this.onReply,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool checkMessageMergeable(Event? a, Event? b) {
|
bool _checkMessageMergeable(Event? a, Event? b) {
|
||||||
if (a == null || b == null) return false;
|
if (a == null || b == null) return false;
|
||||||
if (a.sender.account.id != b.sender.account.id) return false;
|
if (a.sender.account.id != b.sender.account.id) return false;
|
||||||
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
||||||
@ -42,13 +42,13 @@ class ChatEventList extends StatelessWidget {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
bool isMerged = false, hasMerged = false;
|
bool isMerged = false, hasMerged = false;
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
hasMerged = checkMessageMergeable(
|
hasMerged = _checkMessageMergeable(
|
||||||
chatController.currentEvents[index - 1].data,
|
chatController.currentEvents[index - 1].data,
|
||||||
chatController.currentEvents[index].data,
|
chatController.currentEvents[index].data,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (index + 1 < chatController.currentEvents.length) {
|
if (index + 1 < chatController.currentEvents.length) {
|
||||||
isMerged = checkMessageMergeable(
|
isMerged = _checkMessageMergeable(
|
||||||
chatController.currentEvents[index].data,
|
chatController.currentEvents[index].data,
|
||||||
chatController.currentEvents[index + 1].data,
|
chatController.currentEvents[index + 1].data,
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user