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",
|
||||
"addAudio": "Add audio",
|
||||
"addFile": "Add file",
|
||||
"uploadFile": "Upload File",
|
||||
"recordAudio": "Record Audio",
|
||||
"linkAttachment": "Link Attachment",
|
||||
"fileIdCannotBeEmpty": "File ID cannot be empty",
|
||||
|
@@ -122,6 +122,7 @@
|
||||
"addVideo": "添加视频",
|
||||
"addPhoto": "添加照片",
|
||||
"addFile": "添加文件",
|
||||
"uploadFile": "上传文件",
|
||||
"createDirectMessage": "创建新私人消息",
|
||||
"gotoDirectMessage": "前往私信",
|
||||
"react": "反应",
|
||||
|
@@ -122,6 +122,7 @@
|
||||
"addVideo": "添加視頻",
|
||||
"addPhoto": "添加照片",
|
||||
"addFile": "添加文件",
|
||||
"uploadFile": "上傳文件",
|
||||
"createDirectMessage": "創建新私人消息",
|
||||
"gotoDirectMessage": "前往私信",
|
||||
"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({
|
||||
required UniversalFile fileData,
|
||||
required String atk,
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.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 {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.image,
|
||||
@@ -479,8 +504,9 @@ class ComposeLogic {
|
||||
static Future<void> uploadAttachment(
|
||||
WidgetRef ref,
|
||||
ComposeState state,
|
||||
int index,
|
||||
) async {
|
||||
int index, {
|
||||
String? poolId,
|
||||
}) async {
|
||||
final attachment = state.attachments.value[index];
|
||||
if (attachment.isOnCloud) return;
|
||||
|
||||
@@ -489,42 +515,61 @@ class ComposeLogic {
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
|
||||
try {
|
||||
// Update progress state
|
||||
state.attachmentProgress.value = {
|
||||
...state.attachmentProgress.value,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
// Upload file to cloud
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
fileData: attachment,
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: attachment.data.name ?? 'Post media',
|
||||
mimetype:
|
||||
attachment.data.mimeType ??
|
||||
getMimeTypeFromFileType(attachment.type),
|
||||
onProgress: (progress, _) {
|
||||
state.attachmentProgress.value = {
|
||||
...state.attachmentProgress.value,
|
||||
index: progress,
|
||||
};
|
||||
},
|
||||
).future;
|
||||
SnCloudFile? 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(
|
||||
fileData: attachment,
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: attachment.data.name ?? 'Post media',
|
||||
mimetype:
|
||||
attachment.data.mimeType ??
|
||||
getMimeTypeFromFileType(attachment.type),
|
||||
onProgress: (progress, _) {
|
||||
state.attachmentProgress.value = {
|
||||
...state.attachmentProgress.value,
|
||||
index: progress,
|
||||
};
|
||||
},
|
||||
).future;
|
||||
}
|
||||
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
}
|
||||
|
||||
// Update attachments list with cloud file
|
||||
final clone = List.of(state.attachments.value);
|
||||
clone[index] = UniversalFile(data: cloudFile, type: attachment.type);
|
||||
state.attachments.value = clone;
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
showErrorAlert(err.toString());
|
||||
} finally {
|
||||
// Clean up progress state
|
||||
state.attachmentProgress.value = {...state.attachmentProgress.value}
|
||||
..remove(index);
|
||||
}
|
||||
@@ -643,7 +688,6 @@ class ComposeLogic {
|
||||
.where((entry) => entry.value.isOnDevice)
|
||||
.map((entry) => uploadAttachment(ref, state, entry.key)),
|
||||
);
|
||||
|
||||
// Prepare API request
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final isNewPost = originalPost == null;
|
||||
|
@@ -25,6 +25,10 @@ class ComposeToolbar extends HookConsumerWidget {
|
||||
ComposeLogic.pickVideoMedia(ref, state);
|
||||
}
|
||||
|
||||
void pickGeneralFile() {
|
||||
ComposeLogic.pickGeneralFile(ref, state);
|
||||
}
|
||||
|
||||
void addAudio() {
|
||||
ComposeLogic.recordAudioMedia(ref, state, context);
|
||||
}
|
||||
@@ -96,6 +100,12 @@ class ComposeToolbar extends HookConsumerWidget {
|
||||
icon: const Icon(Symbols.mic),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: pickGeneralFile,
|
||||
tooltip: 'uploadFile'.tr(),
|
||||
icon: const Icon(Symbols.file_upload),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: linkAttachment,
|
||||
icon: const Icon(Symbols.attach_file),
|
||||
|
Reference in New Issue
Block a user