Compare commits

...

3 Commits

Author SHA1 Message Date
e4019dadc8 💄 Optimize file upload prograss indicates 2025-11-09 01:59:24 +08:00
5e7d77e1a1 🐛 Fix share sheet error 2025-11-08 20:05:18 +08:00
bfcbed035c ♻️ Refactored file uploading 2025-11-08 20:04:54 +08:00
15 changed files with 208 additions and 121 deletions

View File

@@ -1,7 +1,5 @@
PODS: PODS:
- Alamofire (5.10.2) - Alamofire (5.10.2)
- app_links (6.4.1):
- Flutter
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- croppy (0.0.1): - croppy (0.0.1):
@@ -52,18 +50,18 @@ PODS:
- Firebase/Messaging (12.4.0): - Firebase/Messaging (12.4.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 12.4.0) - FirebaseMessaging (~> 12.4.0)
- firebase_analytics (12.0.3): - firebase_analytics (12.0.4):
- firebase_core - firebase_core
- FirebaseAnalytics (= 12.4.0) - FirebaseAnalytics (= 12.4.0)
- Flutter - Flutter
- firebase_core (4.2.0): - firebase_core (4.2.1):
- Firebase/CoreOnly (= 12.4.0) - Firebase/CoreOnly (= 12.4.0)
- Flutter - Flutter
- firebase_crashlytics (5.0.3): - firebase_crashlytics (5.0.4):
- Firebase/Crashlytics (= 12.4.0) - Firebase/Crashlytics (= 12.4.0)
- firebase_core - firebase_core
- Flutter - Flutter
- firebase_messaging (16.0.3): - firebase_messaging (16.0.4):
- Firebase/Messaging (= 12.4.0) - Firebase/Messaging (= 12.4.0)
- firebase_core - firebase_core
- Flutter - Flutter
@@ -265,6 +263,8 @@ PODS:
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
- PromisesSwift (2.4.0): - PromisesSwift (2.4.0):
- PromisesObjC (= 2.4.0) - PromisesObjC (= 2.4.0)
- protocol_handler_ios (0.0.1):
- Flutter
- receive_sharing_intent (1.8.1): - receive_sharing_intent (1.8.1):
- Flutter - Flutter
- record_ios (1.1.0): - record_ios (1.1.0):
@@ -323,7 +323,6 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- Alamofire - Alamofire
- app_links (from `.symlinks/plugins/app_links/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- croppy (from `.symlinks/plugins/croppy/ios`) - croppy (from `.symlinks/plugins/croppy/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
@@ -358,6 +357,7 @@ DEPENDENCIES:
- pasteboard (from `.symlinks/plugins/pasteboard/ios`) - pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`) - pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- record_ios (from `.symlinks/plugins/record_ios/ios`) - record_ios (from `.symlinks/plugins/record_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
@@ -404,8 +404,6 @@ SPEC REPOS:
- WebRTC-SDK - WebRTC-SDK
EXTERNAL SOURCES: EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
connectivity_plus: connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios" :path: ".symlinks/plugins/connectivity_plus/ios"
croppy: croppy:
@@ -470,6 +468,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
pointer_interceptor_ios: pointer_interceptor_ios:
:path: ".symlinks/plugins/pointer_interceptor_ios/ios" :path: ".symlinks/plugins/pointer_interceptor_ios/ios"
protocol_handler_ios:
:path: ".symlinks/plugins/protocol_handler_ios/ios"
receive_sharing_intent: receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios" :path: ".symlinks/plugins/receive_sharing_intent/ios"
record_ios: record_ios:
@@ -497,7 +497,6 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30 croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
@@ -506,10 +505,10 @@ SPEC CHECKSUMS:
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
firebase_analytics: 1d024068b1d4707d5ba7a42a12976ddf3316d835 firebase_analytics: 67fbdd9f3c04e55048024f3da21cfc36f05e56cf
firebase_core: 744984dbbed8b3036abf34f0b98d80f130a7e464 firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
firebase_crashlytics: f3a9a4338ab99b67042f64e9e22e1bf349cb44ed firebase_crashlytics: 83c7467d7534975a4d779af43bd226d0a4616464
firebase_messaging: 82c70650c426a0a14873e1acdb9ec2b443c4e8b4 firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3 FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018 FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018
@@ -553,6 +552,7 @@ SPEC CHECKSUMS:
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0 pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
protocol_handler_ios: 59f23ee71f3ec602d67902ca7f669a80957888d5
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374 record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c

View File

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

View File

@@ -2,6 +2,7 @@ import "dart:async";
import "dart:math" as math; import "dart:math" as math;
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";
import "package:image_picker/image_picker.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:go_router/go_router.dart"; import "package:go_router/go_router.dart";
import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_hooks/flutter_hooks.dart";
@@ -148,7 +149,7 @@ class ChatRoomScreen extends HookConsumerWidget {
final messageForwardingTo = useState<SnChatMessage?>(null); final messageForwardingTo = useState<SnChatMessage?>(null);
final messageEditingTo = useState<SnChatMessage?>(null); final messageEditingTo = useState<SnChatMessage?>(null);
final attachments = useState<List<UniversalFile>>([]); final attachments = useState<List<UniversalFile>>([]);
final attachmentProgress = useState<Map<String, Map<int, double>>>({}); final attachmentProgress = useState<Map<String, Map<int, double?>>>({});
// Selection mode state // Selection mode state
final isSelectionMode = useState<bool>(false); final isSelectionMode = useState<bool>(false);
@@ -181,16 +182,13 @@ class ChatRoomScreen extends HookConsumerWidget {
}, [scrollController]); }, [scrollController]);
Future<void> pickPhotoMedia() async { Future<void> pickPhotoMedia() async {
final result = await FilePicker.platform.pickFiles( final ImagePicker picker = ImagePicker();
type: FileType.image, final List<XFile> results = await picker.pickMultiImage();
allowMultiple: true, if (results.isEmpty) return;
allowCompression: false,
);
if (result == null || result.count == 0) return;
attachments.value = [ attachments.value = [
...attachments.value, ...attachments.value,
...result.files.map( ...results.map(
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.image), (xfile) => UniversalFile(data: xfile, type: UniversalFileType.image),
), ),
]; ];
} }
@@ -573,7 +571,7 @@ class ChatRoomScreen extends HookConsumerWidget {
onProgress: (progress, _) { onProgress: (progress, _) {
attachmentProgress.value = { attachmentProgress.value = {
...attachmentProgress.value, ...attachmentProgress.value,
'chat-upload': {index: progress}, 'chat-upload': {index: progress ?? 0.0},
}; };
}, },
).future; ).future;

View File

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

View File

@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:convert/convert.dart';
import 'package:cross_file/cross_file.dart'; import 'package:cross_file/cross_file.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
@@ -21,9 +22,51 @@ class FileUploader {
return digest.toString(); return digest.toString();
} }
/// Calculates the MD5 hash from a stream.
Future<String> _calculateFileHashFromStream(Stream<List<int>> stream) async {
final accumulator = AccumulatorSink<Digest>();
final converter = md5.startChunkedConversion(accumulator);
await for (final chunk in stream) {
converter.add(chunk);
}
converter.close();
final digest = accumulator.events.single;
return digest.toString();
}
/// Reads the next chunk from a stream subscription.
Future<Uint8List> _readNextChunk(
StreamSubscription<List<int>> subscription,
int size,
) async {
final completer = Completer<Uint8List>();
final buffer = <int>[];
int remaining = size;
void onData(List<int> data) {
buffer.addAll(data);
remaining -= data.length;
if (remaining <= 0) {
subscription.pause();
completer.complete(Uint8List.fromList(buffer.sublist(0, size)));
}
}
void onDone() {
if (!completer.isCompleted) {
completer.complete(Uint8List.fromList(buffer));
}
}
subscription.onData(onData);
subscription.onDone(onDone);
return completer.future;
}
/// Creates an upload task for the given file. /// Creates an upload task for the given file.
Future<Map<String, dynamic>> createUploadTask({ Future<Map<String, dynamic>> createUploadTask({
required Uint8List bytes, required dynamic fileData,
required String fileName, required String fileName,
required String contentType, required String contentType,
String? poolId, String? poolId,
@@ -32,8 +75,17 @@ class FileUploader {
String? expiredAt, String? expiredAt,
int? chunkSize, int? chunkSize,
}) async { }) async {
final hash = _calculateFileHash(bytes); String hash;
final fileSize = bytes.length; int fileSize;
if (fileData is XFile) {
fileSize = await fileData.length();
hash = await _calculateFileHashFromStream(fileData.openRead());
} else if (fileData is Uint8List) {
hash = _calculateFileHash(fileData);
fileSize = fileData.length;
} else {
throw ArgumentError('Invalid fileData type');
}
final response = await _client.post( final response = await _client.post(
'/drive/files/upload/create', '/drive/files/upload/create',
@@ -58,6 +110,7 @@ class FileUploader {
required String taskId, required String taskId,
required int chunkIndex, required int chunkIndex,
required Uint8List chunkData, required Uint8List chunkData,
ProgressCallback? onSendProgress,
}) async { }) async {
final formData = FormData.fromMap({ final formData = FormData.fromMap({
'chunk': MultipartFile.fromBytes( 'chunk': MultipartFile.fromBytes(
@@ -69,6 +122,7 @@ class FileUploader {
await _client.post( await _client.post(
'/drive/files/upload/chunk/$taskId/$chunkIndex', '/drive/files/upload/chunk/$taskId/$chunkIndex',
data: formData, data: formData,
onSendProgress: onSendProgress,
); );
} }
@@ -81,7 +135,7 @@ class FileUploader {
/// Uploads a file in chunks using the multi-part API. /// Uploads a file in chunks using the multi-part API.
Future<SnCloudFile> uploadFile({ Future<SnCloudFile> uploadFile({
required Uint8List bytes, required dynamic fileData,
required String fileName, required String fileName,
required String contentType, required String contentType,
String? poolId, String? poolId,
@@ -89,10 +143,12 @@ class FileUploader {
String? encryptPassword, String? encryptPassword,
String? expiredAt, String? expiredAt,
int? customChunkSize, int? customChunkSize,
Function(double? progress, Duration estimate)? onProgress,
}) async { }) async {
// Step 1: Create upload task // Step 1: Create upload task
onProgress?.call(null, Duration.zero);
final createResponse = await createUploadTask( final createResponse = await createUploadTask(
bytes: bytes, fileData: fileData,
fileName: fileName, fileName: fileName,
contentType: contentType, contentType: contentType,
poolId: poolId, poolId: poolId,
@@ -110,27 +166,64 @@ class FileUploader {
final taskId = createResponse['task_id'] as String; final taskId = createResponse['task_id'] as String;
final chunkSize = createResponse['chunk_size'] as int; final chunkSize = createResponse['chunk_size'] as int;
final chunksCount = createResponse['chunks_count'] as int; final chunksCount = createResponse['chunks_count'] as int;
int totalSize;
// Step 2: Upload chunks if (fileData is XFile) {
final chunks = <Uint8List>[]; totalSize = await fileData.length();
for (int i = 0; i < bytes.length; i += chunkSize) { } else if (fileData is Uint8List) {
final end = i + chunkSize > bytes.length ? bytes.length : i + chunkSize; totalSize = fileData.length;
chunks.add(Uint8List.fromList(bytes.sublist(i, end))); } else {
throw ArgumentError('Invalid fileData type');
} }
// Ensure we have the correct number of chunks // Step 2: Upload chunks
if (chunks.length != chunksCount) { int bytesUploaded = 0;
throw Exception( if (fileData is XFile) {
'Chunk count mismatch: expected $chunksCount, got ${chunks.length}', // Use stream for XFile
final subscription = fileData.openRead().listen(null);
subscription.pause();
for (int i = 0; i < chunksCount; i++) {
subscription.resume();
final chunkData = await _readNextChunk(subscription, chunkSize);
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) {
// Use old way for Uint8List
final chunks = <Uint8List>[];
for (int i = 0; i < fileData.length; i += chunkSize) {
final end =
i + chunkSize > fileData.length ? fileData.length : i + chunkSize;
chunks.add(Uint8List.fromList(fileData.sublist(i, end)));
} }
// Upload each chunk // Upload each chunk
for (int i = 0; i < chunks.length; i++) { 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 // Step 3: Complete upload
onProgress?.call(null, Duration.zero);
return await completeUpload(taskId); return await completeUpload(taskId);
} }
@@ -139,7 +232,7 @@ class FileUploader {
required Dio client, required Dio client,
String? poolId, String? poolId,
FileUploadMode? mode, FileUploadMode? mode,
Function(double progress, Duration estimate)? onProgress, Function(double? progress, Duration estimate)? onProgress,
}) { }) {
final completer = Completer<SnCloudFile?>(); final completer = Completer<SnCloudFile?>();
@@ -205,7 +298,7 @@ class FileUploader {
UniversalFile fileData, UniversalFile fileData,
Dio client, Dio client,
String? poolId, String? poolId,
Function(double progress, Duration estimate)? onProgress, Function(double? progress, Duration estimate)? onProgress,
Completer<SnCloudFile?> completer, Completer<SnCloudFile?> completer,
) { ) {
String actualMimetype = getMimeType(fileData); String actualMimetype = getMimeType(fileData);
@@ -216,12 +309,8 @@ class FileUploader {
final data = fileData.data; final data = fileData.data;
if (data is XFile) { if (data is XFile) {
// Read bytes from XFile
data
.readAsBytes()
.then((readBytes) {
_performUpload( _performUpload(
bytes: readBytes, fileData: data,
fileName: fileData.displayName ?? data.name, fileName: fileData.displayName ?? data.name,
contentType: actualMimetype, contentType: actualMimetype,
client: client, client: client,
@@ -229,10 +318,6 @@ class FileUploader {
onProgress: onProgress, onProgress: onProgress,
completer: completer, completer: completer,
); );
})
.catchError((e) {
completer.completeError(e);
});
return completer; return completer;
} else if (data is List<int> || data is Uint8List) { } else if (data is List<int> || data is Uint8List) {
bytes = data is List<int> ? Uint8List.fromList(data) : data; bytes = data is List<int> ? Uint8List.fromList(data) : data;
@@ -252,7 +337,7 @@ class FileUploader {
if (bytes != null) { if (bytes != null) {
_performUpload( _performUpload(
bytes: bytes, fileData: bytes,
fileName: actualFilename, fileName: actualFilename,
contentType: actualMimetype, contentType: actualMimetype,
client: client, client: client,
@@ -267,28 +352,29 @@ class FileUploader {
// Helper method to perform the actual upload // Helper method to perform the actual upload
static void _performUpload({ static void _performUpload({
required Uint8List bytes, required dynamic fileData,
required String fileName, required String fileName,
required String contentType, required String contentType,
required Dio client, required Dio client,
String? poolId, String? poolId,
Function(double progress, Duration estimate)? onProgress, Function(double? progress, Duration estimate)? onProgress,
required Completer<SnCloudFile?> completer, required Completer<SnCloudFile?> completer,
}) { }) {
final uploader = FileUploader(client); final uploader = FileUploader(client);
// Call progress start // Call progress start
onProgress?.call(0.0, Duration.zero); onProgress?.call(null, Duration.zero);
uploader uploader
.uploadFile( .uploadFile(
bytes: bytes, fileData: fileData,
fileName: fileName, fileName: fileName,
contentType: contentType, contentType: contentType,
poolId: poolId, poolId: poolId,
onProgress: onProgress,
) )
.then((result) { .then((result) {
// Call progress end // Call progress end
onProgress?.call(1.0, Duration.zero); onProgress?.call(null, Duration.zero);
completer.complete(result); completer.complete(result);
}) })
.catchError((e) { .catchError((e) {

View File

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

View File

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

View File

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

View File

@@ -112,23 +112,28 @@ class CloudFilePicker extends HookConsumerWidget {
void pickImage() async { void pickImage() async {
showLoadingModal(context); showLoadingModal(context);
final result = await FilePicker.platform.pickFiles( final ImagePicker picker = ImagePicker();
allowMultiple: allowMultiple, List<XFile> results;
type: FileType.image, if (allowMultiple) {
results = await picker.pickMultiImage();
} else {
final XFile? result = await picker.pickImage(
source: ImageSource.gallery,
); );
if (result == null || result.files.isEmpty) { results = result != null ? [result] : [];
}
if (results.isEmpty) {
if (context.mounted) hideLoadingModal(context); if (context.mounted) hideLoadingModal(context);
return; return;
} }
final newFiles = final newFiles =
result.files.map((e) { results
final xfile = .map(
e.bytes != null (xfile) =>
? XFile.fromData(e.bytes!, name: e.name) UniversalFile(data: xfile, type: UniversalFileType.image),
: XFile(e.path!); )
return UniversalFile(data: xfile, type: UniversalFileType.image); .toList();
}).toList();
if (!allowMultiple) { if (!allowMultiple) {
files.value = newFiles; files.value = newFiles;

View File

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

View File

@@ -33,7 +33,7 @@ class ComposeState {
final TextEditingController slugController; final TextEditingController slugController;
final ValueNotifier<int> visibility; final ValueNotifier<int> visibility;
final ValueNotifier<List<UniversalFile>> attachments; final ValueNotifier<List<UniversalFile>> attachments;
final ValueNotifier<Map<int, double>> attachmentProgress; final ValueNotifier<Map<int, double?>> attachmentProgress;
final ValueNotifier<SnPublisher?> currentPublisher; final ValueNotifier<SnPublisher?> currentPublisher;
final ValueNotifier<bool> submitting; final ValueNotifier<bool> submitting;
final ValueNotifier<List<SnPostCategory>> categories; final ValueNotifier<List<SnPostCategory>> categories;
@@ -402,16 +402,13 @@ class ComposeLogic {
} }
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 ImagePicker picker = ImagePicker();
type: FileType.image, final List<XFile> results = await picker.pickMultiImage();
allowMultiple: true, if (results.isEmpty) return;
allowCompression: false,
);
if (result == null || result.count == 0) return;
state.attachments.value = [ state.attachments.value = [
...state.attachments.value, ...state.attachments.value,
...result.files.map( ...results.map(
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.image), (xfile) => UniversalFile(data: xfile, type: UniversalFileType.image),
), ),
]; ];
} }
@@ -523,7 +520,7 @@ class ComposeLogic {
onProgress: (progress, _) { onProgress: (progress, _) {
state.attachmentProgress.value = { state.attachmentProgress.value = {
...state.attachmentProgress.value, ...state.attachmentProgress.value,
index: progress, index: progress ?? 0.0,
}; };
}, },
).future; ).future;

View File

@@ -246,7 +246,8 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
onProgress: (progress, _) { onProgress: (progress, _) {
if (mounted) { if (mounted) {
setState(() { setState(() {
_fileUploadProgress[messageId]?[idx] = progress; _fileUploadProgress[messageId]?[idx] =
progress ?? 0.0;
}); });
} }
}, },
@@ -306,7 +307,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
// Navigate to chat if requested // Navigate to chat if requested
if (shouldNavigate == true && mounted) { if (shouldNavigate == true && mounted) {
context.push('/sphere/chat/${chatRoom.id}'); context.push('/chat/${chatRoom.id}');
} }
} }
} catch (e) { } catch (e) {

View File

@@ -19,19 +19,19 @@ PODS:
- Firebase/Messaging (12.4.0): - Firebase/Messaging (12.4.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 12.4.0) - FirebaseMessaging (~> 12.4.0)
- firebase_analytics (12.0.3): - firebase_analytics (12.0.4):
- firebase_core - firebase_core
- FirebaseAnalytics (= 12.4.0) - FirebaseAnalytics (= 12.4.0)
- FlutterMacOS - FlutterMacOS
- firebase_core (4.2.0): - firebase_core (4.2.1):
- Firebase/CoreOnly (~> 12.4.0) - Firebase/CoreOnly (~> 12.4.0)
- FlutterMacOS - FlutterMacOS
- firebase_crashlytics (5.0.3): - firebase_crashlytics (5.0.4):
- Firebase/CoreOnly (~> 12.4.0) - Firebase/CoreOnly (~> 12.4.0)
- Firebase/Crashlytics (~> 12.4.0) - Firebase/Crashlytics (~> 12.4.0)
- firebase_core - firebase_core
- FlutterMacOS - FlutterMacOS
- firebase_messaging (16.0.3): - firebase_messaging (16.0.4):
- Firebase/CoreOnly (~> 12.4.0) - Firebase/CoreOnly (~> 12.4.0)
- Firebase/Messaging (~> 12.4.0) - Firebase/Messaging (~> 12.4.0)
- firebase_core - firebase_core
@@ -416,10 +416,10 @@ SPEC CHECKSUMS:
file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f
file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
firebase_analytics: d876586269c1d8d2b3dcac085bc2d97c62abc9df firebase_analytics: 09241c4796c1c42a02349ef8bf30025f5b640f0e
firebase_core: d81d1a44df95699ce074ae63d8cb43e9df21e142 firebase_core: e054894ab56033ef9bcbe2d9eac9395e5306e2fc
firebase_crashlytics: 723622cc39a9fa7320585424f5864c5699893ce1 firebase_crashlytics: c2438b5f5bdcacf59d0eaee5852c6b0ab09dab77
firebase_messaging: 31f412ae5a54e02d1c46d467969f7ad92c4b81ec firebase_messaging: 373ac3a56e5aa37bb9aff4127f700aa5973c1168
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3 FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018 FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018

View File

@@ -282,7 +282,7 @@ packages:
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
convert: convert:
dependency: transitive dependency: "direct main"
description: description:
name: convert name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
@@ -1185,10 +1185,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: d8f590a69729f719177ea68eb1e598295e8dbc41bbc247fed78b2c8a25660d7c sha256: c92d18e1fe994cb06d48aa786c46b142a5633067e8297cff6b5a3ac742620104
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "16.3.0" version: "17.0.0"
google_fonts: google_fonts:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -38,7 +38,7 @@ dependencies:
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
flutter_hooks: ^0.21.3+1 flutter_hooks: ^0.21.3+1
hooks_riverpod: ^2.6.1 hooks_riverpod: ^2.6.1
go_router: ^16.3.0 go_router: ^17.0.0
styled_widget: ^0.4.1 styled_widget: ^0.4.1
shared_preferences: ^2.5.3 shared_preferences: ^2.5.3
flutter_riverpod: ^2.6.1 flutter_riverpod: ^2.6.1
@@ -165,6 +165,7 @@ dependencies:
dio_smart_retry: ^7.0.1 dio_smart_retry: ^7.0.1
flutter_expandable_fab: ^2.5.2 flutter_expandable_fab: ^2.5.2
event_bus: ^2.0.1 event_bus: ^2.0.1
convert: ^3.1.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: