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:
Texas0295
2025-09-21 14:44:18 +08:00
parent 406e5187a8
commit 269a64cabb
6 changed files with 130 additions and 24 deletions

View File

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