💄 Optimize file upload prograss indicates

This commit is contained in:
2025-11-09 01:59:24 +08:00
parent 5e7d77e1a1
commit e4019dadc8
10 changed files with 61 additions and 28 deletions

View File

@@ -28,7 +28,7 @@ class MessagesNotifier extends _$MessagesNotifier {
late final SnChatMember _identity;
final Map<String, LocalChatMessage> _pendingMessages = {};
final Map<String, Map<int, double>> _fileUploadProgress = {};
final Map<String, Map<int, double?>> _fileUploadProgress = {};
int? _totalCount;
String? _searchQuery;
bool? _withLinks;
@@ -438,7 +438,7 @@ class MessagesNotifier extends _$MessagesNotifier {
SnChatMessage? editingTo,
SnChatMessage? forwardingTo,
SnChatMessage? replyingTo,
Function(String, Map<int, double>)? onProgress,
Function(String, Map<int, double?>)? onProgress,
}) async {
final nonce = const Uuid().v4();
talker.log('Sending message with nonce $nonce');
@@ -474,7 +474,7 @@ class MessagesNotifier extends _$MessagesNotifier {
fileData: attachments[idx],
client: ref.read(apiClientProvider),
onProgress: (progress, _) {
_fileUploadProgress[localMessage.id]?[idx] = progress;
_fileUploadProgress[localMessage.id]?[idx] = progress ?? 0.0;
onProgress?.call(
localMessage.id,
_fileUploadProgress[localMessage.id] ?? {},

View File

@@ -149,7 +149,7 @@ class ChatRoomScreen extends HookConsumerWidget {
final messageForwardingTo = useState<SnChatMessage?>(null);
final messageEditingTo = useState<SnChatMessage?>(null);
final attachments = useState<List<UniversalFile>>([]);
final attachmentProgress = useState<Map<String, Map<int, double>>>({});
final attachmentProgress = useState<Map<String, Map<int, double?>>>({});
// Selection mode state
final isSelectionMode = useState<bool>(false);
@@ -571,7 +571,7 @@ class ChatRoomScreen extends HookConsumerWidget {
onProgress: (progress, _) {
attachmentProgress.value = {
...attachmentProgress.value,
'chat-upload': {index: progress},
'chat-upload': {index: progress ?? 0.0},
};
},
).future;

View File

@@ -306,7 +306,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
],
),
children: [
ValueListenableBuilder<Map<int, double>>(
ValueListenableBuilder<Map<int, double?>>(
valueListenable: state.attachmentProgress,
builder: (context, progressMap, _) {
return Wrap(

View File

@@ -110,6 +110,7 @@ class FileUploader {
required String taskId,
required int chunkIndex,
required Uint8List chunkData,
ProgressCallback? onSendProgress,
}) async {
final formData = FormData.fromMap({
'chunk': MultipartFile.fromBytes(
@@ -121,6 +122,7 @@ class FileUploader {
await _client.post(
'/drive/files/upload/chunk/$taskId/$chunkIndex',
data: formData,
onSendProgress: onSendProgress,
);
}
@@ -141,8 +143,10 @@ class FileUploader {
String? encryptPassword,
String? expiredAt,
int? customChunkSize,
Function(double? progress, Duration estimate)? onProgress,
}) async {
// Step 1: Create upload task
onProgress?.call(null, Duration.zero);
final createResponse = await createUploadTask(
fileData: fileData,
fileName: fileName,
@@ -162,8 +166,17 @@ class FileUploader {
final taskId = createResponse['task_id'] as String;
final chunkSize = createResponse['chunk_size'] as int;
final chunksCount = createResponse['chunks_count'] as int;
int totalSize;
if (fileData is XFile) {
totalSize = await fileData.length();
} else if (fileData is Uint8List) {
totalSize = fileData.length;
} else {
throw ArgumentError('Invalid fileData type');
}
// Step 2: Upload chunks
int bytesUploaded = 0;
if (fileData is XFile) {
// Use stream for XFile
final subscription = fileData.openRead().listen(null);
@@ -171,7 +184,16 @@ class FileUploader {
for (int i = 0; i < chunksCount; i++) {
subscription.resume();
final chunkData = await _readNextChunk(subscription, chunkSize);
await uploadChunk(taskId: taskId, chunkIndex: i, chunkData: chunkData);
await uploadChunk(
taskId: taskId,
chunkIndex: i,
chunkData: chunkData,
onSendProgress: (sent, total) {
final overallProgress = (bytesUploaded + sent) / totalSize;
onProgress?.call(overallProgress, Duration.zero);
},
);
bytesUploaded += chunkData.length;
}
subscription.cancel();
} else if (fileData is Uint8List) {
@@ -185,13 +207,23 @@ class FileUploader {
// Upload each chunk
for (int i = 0; i < chunks.length; i++) {
await uploadChunk(taskId: taskId, chunkIndex: i, chunkData: chunks[i]);
await uploadChunk(
taskId: taskId,
chunkIndex: i,
chunkData: chunks[i],
onSendProgress: (sent, total) {
final overallProgress = (bytesUploaded + sent) / totalSize;
onProgress?.call(overallProgress, Duration.zero);
},
);
bytesUploaded += chunks[i].length;
}
} else {
throw ArgumentError('Invalid fileData type');
}
// Step 3: Complete upload
onProgress?.call(null, Duration.zero);
return await completeUpload(taskId);
}
@@ -200,7 +232,7 @@ class FileUploader {
required Dio client,
String? poolId,
FileUploadMode? mode,
Function(double progress, Duration estimate)? onProgress,
Function(double? progress, Duration estimate)? onProgress,
}) {
final completer = Completer<SnCloudFile?>();
@@ -266,7 +298,7 @@ class FileUploader {
UniversalFile fileData,
Dio client,
String? poolId,
Function(double progress, Duration estimate)? onProgress,
Function(double? progress, Duration estimate)? onProgress,
Completer<SnCloudFile?> completer,
) {
String actualMimetype = getMimeType(fileData);
@@ -325,23 +357,24 @@ class FileUploader {
required String contentType,
required Dio client,
String? poolId,
Function(double progress, Duration estimate)? onProgress,
Function(double? progress, Duration estimate)? onProgress,
required Completer<SnCloudFile?> completer,
}) {
final uploader = FileUploader(client);
// Call progress start
onProgress?.call(0.0, Duration.zero);
onProgress?.call(null, Duration.zero);
uploader
.uploadFile(
fileData: fileData,
fileName: fileName,
contentType: contentType,
poolId: poolId,
onProgress: onProgress,
)
.then((result) {
// Call progress end
onProgress?.call(1.0, Duration.zero);
onProgress?.call(null, Duration.zero);
completer.complete(result);
})
.catchError((e) {

View File

@@ -44,7 +44,7 @@ class ChatInput extends HookConsumerWidget {
final Function(int) onDeleteAttachment;
final Function(int, int) onMoveAttachment;
final Function(List<UniversalFile>) onAttachmentsChanged;
final Map<String, Map<int, double>> attachmentProgress;
final Map<String, Map<int, double?>> attachmentProgress;
const ChatInput({
super.key,

View File

@@ -40,7 +40,7 @@ class MessageItem extends HookConsumerWidget {
final LocalChatMessage message;
final bool isCurrentUser;
final Function(String action)? onAction;
final Map<int, double>? progress;
final Map<int, double?>? progress;
final bool showAvatar;
final Function(String messageId) onJump;
final bool isSelectionMode;
@@ -689,7 +689,7 @@ class MessageHoverActionMenu extends StatelessWidget {
class MessageItemDisplayBubble extends HookConsumerWidget {
final LocalChatMessage message;
final bool isCurrentUser;
final Map<int, double>? progress;
final Map<int, double?>? progress;
final bool showAvatar;
final Function(String messageId) onJump;
final String? translatedText;
@@ -821,7 +821,7 @@ class MessageItemDisplayBubble extends HookConsumerWidget {
class MessageItemDisplayIRC extends HookConsumerWidget {
final LocalChatMessage message;
final bool isCurrentUser;
final Map<int, double>? progress;
final Map<int, double?>? progress;
final bool showAvatar;
final Function(String messageId) onJump;
final String? translatedText;
@@ -949,7 +949,7 @@ class MessageItemDisplayIRC extends HookConsumerWidget {
class MessageItemDisplayDiscord extends HookConsumerWidget {
final LocalChatMessage message;
final bool isCurrentUser;
final Map<int, double>? progress;
final Map<int, double?>? progress;
final bool showAvatar;
final Function(String messageId) onJump;
final String? translatedText;
@@ -1238,7 +1238,7 @@ class MessageQuoteWidget extends HookConsumerWidget {
}
class FileUploadProgressWidget extends StatelessWidget {
final Map<int, double>? progress;
final Map<int, double?>? progress;
final Color textColor;
final bool hasContent;
@@ -1266,7 +1266,9 @@ class FileUploadProgressWidget extends StatelessWidget {
'fileUploadingProgress'.tr(
args: [
(entry.key + 1).toString(),
(entry.value * 100).toStringAsFixed(1),
entry.value != null
? (entry.value! * 100).toStringAsFixed(1)
: '0.0',
],
),
style: TextStyle(

View File

@@ -411,10 +411,7 @@ class AttachmentPreview extends HookConsumerWidget {
),
Gap(6),
Center(
child: LinearProgressIndicator(
value:
progress != null ? progress! / 100.0 : null,
),
child: LinearProgressIndicator(value: progress),
),
],
),

View File

@@ -131,7 +131,7 @@ class ArticleComposeAttachments extends ConsumerWidget {
],
),
children: [
ValueListenableBuilder<Map<int, double>>(
ValueListenableBuilder<Map<int, double?>>(
valueListenable: state.attachmentProgress,
builder: (context, progressMap, _) {
return Wrap(

View File

@@ -33,7 +33,7 @@ class ComposeState {
final TextEditingController slugController;
final ValueNotifier<int> visibility;
final ValueNotifier<List<UniversalFile>> attachments;
final ValueNotifier<Map<int, double>> attachmentProgress;
final ValueNotifier<Map<int, double?>> attachmentProgress;
final ValueNotifier<SnPublisher?> currentPublisher;
final ValueNotifier<bool> submitting;
final ValueNotifier<List<SnPostCategory>> categories;
@@ -520,7 +520,7 @@ class ComposeLogic {
onProgress: (progress, _) {
state.attachmentProgress.value = {
...state.attachmentProgress.value,
index: progress,
index: progress ?? 0.0,
};
},
).future;

View File

@@ -246,7 +246,8 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
onProgress: (progress, _) {
if (mounted) {
setState(() {
_fileUploadProgress[messageId]?[idx] = progress;
_fileUploadProgress[messageId]?[idx] =
progress ?? 0.0;
});
}
},