✨ 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 {
|
||||
final AttachmentProvider provider = Get.find();
|
||||
try {
|
||||
final resp = await provider.createAttachment(
|
||||
final result = await provider.createAttachment(
|
||||
data,
|
||||
path,
|
||||
usage,
|
||||
metadata,
|
||||
onProgress: onProgress,
|
||||
);
|
||||
var result = Attachment.fromJson(resp.body);
|
||||
return result;
|
||||
} catch (err) {
|
||||
rethrow;
|
||||
|
@ -6,6 +6,7 @@ import 'package:path/path.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:dio/dio.dart' as dio;
|
||||
|
||||
class AttachmentProvider extends GetConnect {
|
||||
static Map<String, String> mimetypeOverrides = {
|
||||
@ -37,18 +38,14 @@ class AttachmentProvider extends GetConnect {
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Response> createAttachment(
|
||||
Future<Attachment> createAttachment(
|
||||
Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
|
||||
{Function(double)? onProgress}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
|
||||
final client = auth.configureClient(
|
||||
'files',
|
||||
timeout: const Duration(minutes: 3),
|
||||
);
|
||||
|
||||
final filePayload = MultipartFile(data, filename: basename(path));
|
||||
final filePayload =
|
||||
dio.MultipartFile.fromBytes(data, filename: basename(path));
|
||||
final fileAlt = basename(path).contains('.')
|
||||
? basename(path).substring(0, basename(path).lastIndexOf('.'))
|
||||
: basename(path);
|
||||
@ -61,25 +58,31 @@ class AttachmentProvider extends GetConnect {
|
||||
if (mimetypeOverrides.keys.contains(fileExt)) {
|
||||
mimetypeOverride = mimetypeOverrides[fileExt];
|
||||
}
|
||||
final payload = FormData({
|
||||
final payload = dio.FormData.fromMap({
|
||||
'alt': fileAlt,
|
||||
'file': filePayload,
|
||||
'usage': usage,
|
||||
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
|
||||
'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',
|
||||
payload,
|
||||
uploadProgress: (progress) {
|
||||
if (onProgress != null) onProgress(progress);
|
||||
data: payload,
|
||||
onSendProgress: (count, total) {
|
||||
if (onProgress != null) onProgress(count / total);
|
||||
},
|
||||
);
|
||||
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(
|
||||
|
@ -7,6 +7,7 @@ import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
import 'package:solian/services.dart';
|
||||
@ -104,9 +105,9 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
|
||||
final AttachmentProvider provider = Get.find();
|
||||
|
||||
Response? attachResp;
|
||||
Attachment? attachResult;
|
||||
try {
|
||||
attachResp = await provider.createAttachment(
|
||||
attachResult = await provider.createAttachment(
|
||||
await file.readAsBytes(),
|
||||
file.path,
|
||||
'p.$position',
|
||||
@ -122,7 +123,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
|
||||
final resp = await client.put(
|
||||
'/users/me/$position',
|
||||
{'attachment': attachResp.body['id']},
|
||||
{'attachment': attachResult.id},
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
_syncWidget();
|
||||
|
@ -13,6 +13,7 @@ const i18nEnglish = {
|
||||
'more': 'More',
|
||||
'share': 'Share',
|
||||
'feed': 'Feed',
|
||||
'unlink': 'Unlink',
|
||||
'feedSearch': 'Search Feed',
|
||||
'feedSearchWithTag': 'Searching with tag #@key',
|
||||
'feedSearchWithCategory': 'Searching in category @category',
|
||||
@ -171,6 +172,9 @@ const i18nEnglish = {
|
||||
'attachmentAddCameraVideo': 'Capture video',
|
||||
'attachmentAddClipboard': 'Paste file',
|
||||
'attachmentAddFile': 'Attach file',
|
||||
'attachmentAddLink': 'Link attachments',
|
||||
'attachmentAddLinkHint': 'Enter attachment serial number to link that attachment',
|
||||
'attachmentAddLinkInput': 'Serial number',
|
||||
'attachmentSetting': 'Adjust attachment',
|
||||
'attachmentAlt': 'Alternative text',
|
||||
'attachmentLoadFailed': 'Load Attachment Failed',
|
||||
|
@ -21,6 +21,7 @@ const i18nSimplifiedChinese = {
|
||||
'more': '更多',
|
||||
'share': '分享',
|
||||
'feed': '资讯',
|
||||
'unlink': '移除链接',
|
||||
'feedSearch': '搜索资讯',
|
||||
'feedSearchWithTag': '检索带有 #@key 标签的资讯',
|
||||
'feedSearchWithCategory': '检索位于分类 @category 的资讯',
|
||||
@ -160,6 +161,9 @@ const i18nSimplifiedChinese = {
|
||||
'attachmentAddCameraVideo': '拍摄视频',
|
||||
'attachmentAddClipboard': '粘贴文件',
|
||||
'attachmentAddFile': '附加文件',
|
||||
'attachmentAddLink': '链接附件',
|
||||
'attachmentAddLinkHint': '输入附件的神秘代号来链接对应附件',
|
||||
'attachmentAddLinkInput': '神秘代号',
|
||||
'attachmentSetting': '调整附件',
|
||||
'attachmentAlt': '替代文字',
|
||||
'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 {
|
||||
final data = await Pasteboard.image;
|
||||
if (data == null) return;
|
||||
@ -150,7 +207,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
}
|
||||
|
||||
void _revertMetadataList() {
|
||||
final AttachmentProvider provider = Get.find();
|
||||
final AttachmentProvider attach = Get.find();
|
||||
|
||||
if (widget.initialAttachments.isEmpty) {
|
||||
_isFirstTimeBusy = false;
|
||||
@ -167,7 +224,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
|
||||
int progress = 0;
|
||||
for (var idx = 0; idx < widget.initialAttachments.length; idx++) {
|
||||
provider.getMetadata(widget.initialAttachments[idx]).then((resp) {
|
||||
attach.getMetadata(widget.initialAttachments[idx]).then((resp) {
|
||||
progress++;
|
||||
_attachments[idx] = resp;
|
||||
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),
|
||||
onPressed: () => _pickFileToUpload(),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.link),
|
||||
label: Text('attachmentAddFile'.tr),
|
||||
style: const ButtonStyle(visualDensity: density),
|
||||
onPressed: () => _linkAttachments(),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 12),
|
||||
),
|
||||
|
@ -24,7 +24,7 @@ class ChatEventList extends StatelessWidget {
|
||||
required this.onReply,
|
||||
});
|
||||
|
||||
bool checkMessageMergeable(Event? a, Event? b) {
|
||||
bool _checkMessageMergeable(Event? a, Event? b) {
|
||||
if (a == null || b == null) return false;
|
||||
if (a.sender.account.id != b.sender.account.id) return false;
|
||||
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
||||
@ -42,13 +42,13 @@ class ChatEventList extends StatelessWidget {
|
||||
itemBuilder: (context, index) {
|
||||
bool isMerged = false, hasMerged = false;
|
||||
if (index > 0) {
|
||||
hasMerged = checkMessageMergeable(
|
||||
hasMerged = _checkMessageMergeable(
|
||||
chatController.currentEvents[index - 1].data,
|
||||
chatController.currentEvents[index].data,
|
||||
);
|
||||
}
|
||||
if (index + 1 < chatController.currentEvents.length) {
|
||||
isMerged = checkMessageMergeable(
|
||||
isMerged = _checkMessageMergeable(
|
||||
chatController.currentEvents[index].data,
|
||||
chatController.currentEvents[index + 1].data,
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user