Attachment can link exists things

 Optimize upload progress
This commit is contained in:
LittleSheep 2024-08-02 15:49:32 +08:00
parent 98cc313a91
commit 11fb79623e
7 changed files with 111 additions and 24 deletions

View File

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

View File

@ -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(

View File

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

View File

@ -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',

View File

@ -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': '加载失败',

View File

@ -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),
), ),

View File

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