add general file upload support with pool-aware tus client
- add "uploadFile" i18n key (en, zh-CN, zh-TW) - introduce putFileToPool for tus upload with X-FilePool header - add ComposeLogic.pickGeneralFile for arbitrary files - extend uploadAttachment to support poolId override - add toolbar button for general file upload Signed-off-by: Texas0295 <kimura@texas0295.top>
This commit is contained in:
@@ -168,6 +168,7 @@
|
|||||||
"addPhoto": "Add photo",
|
"addPhoto": "Add photo",
|
||||||
"addAudio": "Add audio",
|
"addAudio": "Add audio",
|
||||||
"addFile": "Add file",
|
"addFile": "Add file",
|
||||||
|
"uploadFile": "Upload File",
|
||||||
"recordAudio": "Record Audio",
|
"recordAudio": "Record Audio",
|
||||||
"linkAttachment": "Link Attachment",
|
"linkAttachment": "Link Attachment",
|
||||||
"fileIdCannotBeEmpty": "File ID cannot be empty",
|
"fileIdCannotBeEmpty": "File ID cannot be empty",
|
||||||
|
@@ -122,6 +122,7 @@
|
|||||||
"addVideo": "添加视频",
|
"addVideo": "添加视频",
|
||||||
"addPhoto": "添加照片",
|
"addPhoto": "添加照片",
|
||||||
"addFile": "添加文件",
|
"addFile": "添加文件",
|
||||||
|
"uploadFile": "上传文件",
|
||||||
"createDirectMessage": "创建新私人消息",
|
"createDirectMessage": "创建新私人消息",
|
||||||
"gotoDirectMessage": "前往私信",
|
"gotoDirectMessage": "前往私信",
|
||||||
"react": "反应",
|
"react": "反应",
|
||||||
|
@@ -122,6 +122,7 @@
|
|||||||
"addVideo": "添加視頻",
|
"addVideo": "添加視頻",
|
||||||
"addPhoto": "添加照片",
|
"addPhoto": "添加照片",
|
||||||
"addFile": "添加文件",
|
"addFile": "添加文件",
|
||||||
|
"uploadFile": "上傳文件",
|
||||||
"createDirectMessage": "創建新私人消息",
|
"createDirectMessage": "創建新私人消息",
|
||||||
"gotoDirectMessage": "前往私信",
|
"gotoDirectMessage": "前往私信",
|
||||||
"react": "反應",
|
"react": "反應",
|
||||||
|
@@ -40,6 +40,55 @@ Future<XFile?> cropImage(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Completer<SnCloudFile?> putFileToPool({
|
||||||
|
required UniversalFile fileData,
|
||||||
|
required String atk,
|
||||||
|
required String baseUrl,
|
||||||
|
required String poolId,
|
||||||
|
String? filename,
|
||||||
|
String? mimetype,
|
||||||
|
Function(double progress, Duration estimate)? onProgress,
|
||||||
|
}) {
|
||||||
|
final completer = Completer<SnCloudFile?>();
|
||||||
|
|
||||||
|
final data = fileData.data;
|
||||||
|
if (data is! XFile) {
|
||||||
|
completer.completeError(
|
||||||
|
ArgumentError('Unsupported fileData type for putFileToPool'),
|
||||||
|
);
|
||||||
|
return completer;
|
||||||
|
}
|
||||||
|
|
||||||
|
final actualFilename = filename ?? data.name;
|
||||||
|
final actualMimetype = mimetype ?? data.mimeType ?? 'application/octet-stream';
|
||||||
|
|
||||||
|
final metadata = {
|
||||||
|
'filename': actualFilename,
|
||||||
|
'content-type': actualMimetype,
|
||||||
|
};
|
||||||
|
|
||||||
|
final client = TusClient(data);
|
||||||
|
client
|
||||||
|
.upload(
|
||||||
|
uri: Uri.parse('$baseUrl/drive/tus'),
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'AtField $atk',
|
||||||
|
'X-FilePool': poolId,
|
||||||
|
},
|
||||||
|
metadata: metadata,
|
||||||
|
onComplete: (lastResponse) {
|
||||||
|
final resp = jsonDecode(lastResponse!.headers['x-fileinfo']!);
|
||||||
|
completer.complete(SnCloudFile.fromJson(resp));
|
||||||
|
},
|
||||||
|
onProgress: (progress, est) {
|
||||||
|
onProgress?.call(progress, est);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.catchError(completer.completeError);
|
||||||
|
|
||||||
|
return completer;
|
||||||
|
}
|
||||||
|
|
||||||
Completer<SnCloudFile?> putMediaToCloud({
|
Completer<SnCloudFile?> putMediaToCloud({
|
||||||
required UniversalFile fileData,
|
required UniversalFile fileData,
|
||||||
required String atk,
|
required String atk,
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:mime/mime.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
@@ -386,6 +387,30 @@ class ComposeLogic {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> pickGeneralFile(WidgetRef ref, ComposeState state) async {
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.any,
|
||||||
|
allowMultiple: true,
|
||||||
|
);
|
||||||
|
if (result == null || result.count == 0) return;
|
||||||
|
|
||||||
|
final newFiles = <UniversalFile>[];
|
||||||
|
|
||||||
|
for (final f in result.files) {
|
||||||
|
if (f.path == null) continue;
|
||||||
|
|
||||||
|
final mimeType =
|
||||||
|
lookupMimeType(f.path!, headerBytes: f.bytes) ??
|
||||||
|
'application/octet-stream';
|
||||||
|
final xfile = XFile(f.path!, name: f.name, mimeType: mimeType);
|
||||||
|
|
||||||
|
final uf = UniversalFile(data: xfile, type: UniversalFileType.file);
|
||||||
|
newFiles.add(uf);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.attachments.value = [...state.attachments.value, ...newFiles];
|
||||||
|
}
|
||||||
|
|
||||||
static Future<void> pickPhotoMedia(WidgetRef ref, ComposeState state) async {
|
static Future<void> pickPhotoMedia(WidgetRef ref, ComposeState state) async {
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
type: FileType.image,
|
type: FileType.image,
|
||||||
@@ -479,8 +504,9 @@ class ComposeLogic {
|
|||||||
static Future<void> uploadAttachment(
|
static Future<void> uploadAttachment(
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
ComposeState state,
|
ComposeState state,
|
||||||
int index,
|
int index, {
|
||||||
) async {
|
String? poolId,
|
||||||
|
}) async {
|
||||||
final attachment = state.attachments.value[index];
|
final attachment = state.attachments.value[index];
|
||||||
if (attachment.isOnCloud) return;
|
if (attachment.isOnCloud) return;
|
||||||
|
|
||||||
@@ -489,14 +515,34 @@ class ComposeLogic {
|
|||||||
if (token == null) throw ArgumentError('Token is null');
|
if (token == null) throw ArgumentError('Token is null');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update progress state
|
|
||||||
state.attachmentProgress.value = {
|
state.attachmentProgress.value = {
|
||||||
...state.attachmentProgress.value,
|
...state.attachmentProgress.value,
|
||||||
index: 0,
|
index: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Upload file to cloud
|
SnCloudFile? cloudFile;
|
||||||
final cloudFile =
|
|
||||||
|
if (attachment.type == UniversalFileType.file) {
|
||||||
|
cloudFile =
|
||||||
|
await putFileToPool(
|
||||||
|
fileData: attachment,
|
||||||
|
atk: token,
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
// TODO: Generic Pool ID (Now: Solian Network Driver)
|
||||||
|
poolId: poolId ?? '500e5ed8-bd44-4359-bc0a-ec85e2adf447',
|
||||||
|
filename: attachment.data.name ?? 'General file',
|
||||||
|
mimetype:
|
||||||
|
attachment.data.mimeType ??
|
||||||
|
getMimeTypeFromFileType(attachment.type),
|
||||||
|
onProgress: (progress, _) {
|
||||||
|
state.attachmentProgress.value = {
|
||||||
|
...state.attachmentProgress.value,
|
||||||
|
index: progress,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
).future;
|
||||||
|
} else {
|
||||||
|
cloudFile =
|
||||||
await putMediaToCloud(
|
await putMediaToCloud(
|
||||||
fileData: attachment,
|
fileData: attachment,
|
||||||
atk: token,
|
atk: token,
|
||||||
@@ -512,19 +558,18 @@ class ComposeLogic {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
).future;
|
).future;
|
||||||
|
}
|
||||||
|
|
||||||
if (cloudFile == null) {
|
if (cloudFile == null) {
|
||||||
throw ArgumentError('Failed to upload the file...');
|
throw ArgumentError('Failed to upload the file...');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update attachments list with cloud file
|
|
||||||
final clone = List.of(state.attachments.value);
|
final clone = List.of(state.attachments.value);
|
||||||
clone[index] = UniversalFile(data: cloudFile, type: attachment.type);
|
clone[index] = UniversalFile(data: cloudFile, type: attachment.type);
|
||||||
state.attachments.value = clone;
|
state.attachments.value = clone;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showErrorAlert(err);
|
showErrorAlert(err.toString());
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up progress state
|
|
||||||
state.attachmentProgress.value = {...state.attachmentProgress.value}
|
state.attachmentProgress.value = {...state.attachmentProgress.value}
|
||||||
..remove(index);
|
..remove(index);
|
||||||
}
|
}
|
||||||
@@ -643,7 +688,6 @@ class ComposeLogic {
|
|||||||
.where((entry) => entry.value.isOnDevice)
|
.where((entry) => entry.value.isOnDevice)
|
||||||
.map((entry) => uploadAttachment(ref, state, entry.key)),
|
.map((entry) => uploadAttachment(ref, state, entry.key)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Prepare API request
|
// Prepare API request
|
||||||
final client = ref.watch(apiClientProvider);
|
final client = ref.watch(apiClientProvider);
|
||||||
final isNewPost = originalPost == null;
|
final isNewPost = originalPost == null;
|
||||||
|
@@ -25,6 +25,10 @@ class ComposeToolbar extends HookConsumerWidget {
|
|||||||
ComposeLogic.pickVideoMedia(ref, state);
|
ComposeLogic.pickVideoMedia(ref, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void pickGeneralFile() {
|
||||||
|
ComposeLogic.pickGeneralFile(ref, state);
|
||||||
|
}
|
||||||
|
|
||||||
void addAudio() {
|
void addAudio() {
|
||||||
ComposeLogic.recordAudioMedia(ref, state, context);
|
ComposeLogic.recordAudioMedia(ref, state, context);
|
||||||
}
|
}
|
||||||
@@ -96,6 +100,12 @@ class ComposeToolbar extends HookConsumerWidget {
|
|||||||
icon: const Icon(Symbols.mic),
|
icon: const Icon(Symbols.mic),
|
||||||
color: colorScheme.primary,
|
color: colorScheme.primary,
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: pickGeneralFile,
|
||||||
|
tooltip: 'uploadFile'.tr(),
|
||||||
|
icon: const Icon(Symbols.file_upload),
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: linkAttachment,
|
onPressed: linkAttachment,
|
||||||
icon: const Icon(Symbols.attach_file),
|
icon: const Icon(Symbols.attach_file),
|
||||||
|
Reference in New Issue
Block a user