From bfcbed035c31e9628f7bfa3ed761bdd43a649546 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 8 Nov 2025 20:04:54 +0800 Subject: [PATCH] :recycle: Refactored file uploading --- ios/Podfile.lock | 28 ++--- lib/screens/chat/room.dart | 14 +-- lib/services/file_uploader.dart | 133 ++++++++++++++------- lib/widgets/content/cloud_file_picker.dart | 29 +++-- lib/widgets/post/compose_shared.dart | 13 +- macos/Podfile.lock | 16 +-- pubspec.lock | 6 +- pubspec.yaml | 3 +- 8 files changed, 148 insertions(+), 94 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 50a13ef0..6a161a9c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,7 +1,5 @@ PODS: - Alamofire (5.10.2) - - app_links (6.4.1): - - Flutter - connectivity_plus (0.0.1): - Flutter - croppy (0.0.1): @@ -52,18 +50,18 @@ PODS: - Firebase/Messaging (12.4.0): - Firebase/CoreOnly - FirebaseMessaging (~> 12.4.0) - - firebase_analytics (12.0.3): + - firebase_analytics (12.0.4): - firebase_core - FirebaseAnalytics (= 12.4.0) - Flutter - - firebase_core (4.2.0): + - firebase_core (4.2.1): - Firebase/CoreOnly (= 12.4.0) - Flutter - - firebase_crashlytics (5.0.3): + - firebase_crashlytics (5.0.4): - Firebase/Crashlytics (= 12.4.0) - firebase_core - Flutter - - firebase_messaging (16.0.3): + - firebase_messaging (16.0.4): - Firebase/Messaging (= 12.4.0) - firebase_core - Flutter @@ -265,6 +263,8 @@ PODS: - PromisesObjC (2.4.0) - PromisesSwift (2.4.0): - PromisesObjC (= 2.4.0) + - protocol_handler_ios (0.0.1): + - Flutter - receive_sharing_intent (1.8.1): - Flutter - record_ios (1.1.0): @@ -323,7 +323,6 @@ PODS: DEPENDENCIES: - Alamofire - - app_links (from `.symlinks/plugins/app_links/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - croppy (from `.symlinks/plugins/croppy/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) @@ -358,6 +357,7 @@ DEPENDENCIES: - pasteboard (from `.symlinks/plugins/pasteboard/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - 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`) - record_ios (from `.symlinks/plugins/record_ios/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) @@ -404,8 +404,6 @@ SPEC REPOS: - WebRTC-SDK EXTERNAL SOURCES: - app_links: - :path: ".symlinks/plugins/app_links/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" croppy: @@ -470,6 +468,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" pointer_interceptor_ios: :path: ".symlinks/plugins/pointer_interceptor_ios/ios" + protocol_handler_ios: + :path: ".symlinks/plugins/protocol_handler_ios/ios" receive_sharing_intent: :path: ".symlinks/plugins/receive_sharing_intent/ios" record_ios: @@ -497,7 +497,6 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 - app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30 device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe @@ -506,10 +505,10 @@ SPEC CHECKSUMS: file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e - firebase_analytics: 1d024068b1d4707d5ba7a42a12976ddf3316d835 - firebase_core: 744984dbbed8b3036abf34f0b98d80f130a7e464 - firebase_crashlytics: f3a9a4338ab99b67042f64e9e22e1bf349cb44ed - firebase_messaging: 82c70650c426a0a14873e1acdb9ec2b443c4e8b4 + firebase_analytics: 67fbdd9f3c04e55048024f3da21cfc36f05e56cf + firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594 + firebase_crashlytics: 83c7467d7534975a4d779af43bd226d0a4616464 + firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3 FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018 @@ -553,6 +552,7 @@ SPEC CHECKSUMS: pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 + protocol_handler_ios: 59f23ee71f3ec602d67902ca7f669a80957888d5 receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index e1b8ebc7..82a029e7 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -2,6 +2,7 @@ import "dart:async"; import "dart:math" as math; import "package:easy_localization/easy_localization.dart"; import "package:file_picker/file_picker.dart"; +import "package:image_picker/image_picker.dart"; import "package:flutter/material.dart"; import "package:go_router/go_router.dart"; import "package:flutter_hooks/flutter_hooks.dart"; @@ -181,16 +182,13 @@ class ChatRoomScreen extends HookConsumerWidget { }, [scrollController]); Future pickPhotoMedia() async { - final result = await FilePicker.platform.pickFiles( - type: FileType.image, - allowMultiple: true, - allowCompression: false, - ); - if (result == null || result.count == 0) return; + final ImagePicker picker = ImagePicker(); + final List results = await picker.pickMultiImage(); + if (results.isEmpty) return; attachments.value = [ ...attachments.value, - ...result.files.map( - (e) => UniversalFile(data: e.xFile, type: UniversalFileType.image), + ...results.map( + (xfile) => UniversalFile(data: xfile, type: UniversalFileType.image), ), ]; } diff --git a/lib/services/file_uploader.dart b/lib/services/file_uploader.dart index 938419fd..516edd2c 100644 --- a/lib/services/file_uploader.dart +++ b/lib/services/file_uploader.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:convert/convert.dart'; import 'package:cross_file/cross_file.dart'; import 'package:crypto/crypto.dart'; import 'package:dio/dio.dart'; @@ -21,9 +22,51 @@ class FileUploader { return digest.toString(); } + /// Calculates the MD5 hash from a stream. + Future _calculateFileHashFromStream(Stream> stream) async { + final accumulator = AccumulatorSink(); + 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 _readNextChunk( + StreamSubscription> subscription, + int size, + ) async { + final completer = Completer(); + final buffer = []; + int remaining = size; + + void onData(List 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. Future> createUploadTask({ - required Uint8List bytes, + required dynamic fileData, required String fileName, required String contentType, String? poolId, @@ -32,8 +75,17 @@ class FileUploader { String? expiredAt, int? chunkSize, }) async { - final hash = _calculateFileHash(bytes); - final fileSize = bytes.length; + String hash; + 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( '/drive/files/upload/create', @@ -81,7 +133,7 @@ class FileUploader { /// Uploads a file in chunks using the multi-part API. Future uploadFile({ - required Uint8List bytes, + required dynamic fileData, required String fileName, required String contentType, String? poolId, @@ -92,7 +144,7 @@ class FileUploader { }) async { // Step 1: Create upload task final createResponse = await createUploadTask( - bytes: bytes, + fileData: fileData, fileName: fileName, contentType: contentType, poolId: poolId, @@ -112,22 +164,31 @@ class FileUploader { final chunksCount = createResponse['chunks_count'] as int; // Step 2: Upload chunks - final chunks = []; - for (int i = 0; i < bytes.length; i += chunkSize) { - final end = i + chunkSize > bytes.length ? bytes.length : i + chunkSize; - chunks.add(Uint8List.fromList(bytes.sublist(i, end))); - } + if (fileData is XFile) { + // 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); + } + subscription.cancel(); + } else if (fileData is Uint8List) { + // Use old way for Uint8List + final chunks = []; + 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))); + } - // Ensure we have the correct number of chunks - if (chunks.length != chunksCount) { - throw Exception( - 'Chunk count mismatch: expected $chunksCount, got ${chunks.length}', - ); - } - - // Upload each chunk - for (int i = 0; i < chunks.length; i++) { - await uploadChunk(taskId: taskId, chunkIndex: i, chunkData: chunks[i]); + // Upload each chunk + for (int i = 0; i < chunks.length; i++) { + await uploadChunk(taskId: taskId, chunkIndex: i, chunkData: chunks[i]); + } + } else { + throw ArgumentError('Invalid fileData type'); } // Step 3: Complete upload @@ -216,23 +277,15 @@ class FileUploader { final data = fileData.data; if (data is XFile) { - // Read bytes from XFile - data - .readAsBytes() - .then((readBytes) { - _performUpload( - bytes: readBytes, - fileName: fileData.displayName ?? data.name, - contentType: actualMimetype, - client: client, - poolId: poolId, - onProgress: onProgress, - completer: completer, - ); - }) - .catchError((e) { - completer.completeError(e); - }); + _performUpload( + fileData: data, + fileName: fileData.displayName ?? data.name, + contentType: actualMimetype, + client: client, + poolId: poolId, + onProgress: onProgress, + completer: completer, + ); return completer; } else if (data is List || data is Uint8List) { bytes = data is List ? Uint8List.fromList(data) : data; @@ -252,7 +305,7 @@ class FileUploader { if (bytes != null) { _performUpload( - bytes: bytes, + fileData: bytes, fileName: actualFilename, contentType: actualMimetype, client: client, @@ -267,7 +320,7 @@ class FileUploader { // Helper method to perform the actual upload static void _performUpload({ - required Uint8List bytes, + required dynamic fileData, required String fileName, required String contentType, required Dio client, @@ -281,7 +334,7 @@ class FileUploader { onProgress?.call(0.0, Duration.zero); uploader .uploadFile( - bytes: bytes, + fileData: fileData, fileName: fileName, contentType: contentType, poolId: poolId, diff --git a/lib/widgets/content/cloud_file_picker.dart b/lib/widgets/content/cloud_file_picker.dart index d446e5ad..757b1dce 100644 --- a/lib/widgets/content/cloud_file_picker.dart +++ b/lib/widgets/content/cloud_file_picker.dart @@ -112,23 +112,28 @@ class CloudFilePicker extends HookConsumerWidget { void pickImage() async { showLoadingModal(context); - final result = await FilePicker.platform.pickFiles( - allowMultiple: allowMultiple, - type: FileType.image, - ); - if (result == null || result.files.isEmpty) { + final ImagePicker picker = ImagePicker(); + List results; + if (allowMultiple) { + results = await picker.pickMultiImage(); + } else { + final XFile? result = await picker.pickImage( + source: ImageSource.gallery, + ); + results = result != null ? [result] : []; + } + if (results.isEmpty) { if (context.mounted) hideLoadingModal(context); return; } final newFiles = - result.files.map((e) { - final xfile = - e.bytes != null - ? XFile.fromData(e.bytes!, name: e.name) - : XFile(e.path!); - return UniversalFile(data: xfile, type: UniversalFileType.image); - }).toList(); + results + .map( + (xfile) => + UniversalFile(data: xfile, type: UniversalFileType.image), + ) + .toList(); if (!allowMultiple) { files.value = newFiles; diff --git a/lib/widgets/post/compose_shared.dart b/lib/widgets/post/compose_shared.dart index 1bc8a389..be3509f1 100644 --- a/lib/widgets/post/compose_shared.dart +++ b/lib/widgets/post/compose_shared.dart @@ -402,16 +402,13 @@ class ComposeLogic { } static Future pickPhotoMedia(WidgetRef ref, ComposeState state) async { - final result = await FilePicker.platform.pickFiles( - type: FileType.image, - allowMultiple: true, - allowCompression: false, - ); - if (result == null || result.count == 0) return; + final ImagePicker picker = ImagePicker(); + final List results = await picker.pickMultiImage(); + if (results.isEmpty) return; state.attachments.value = [ ...state.attachments.value, - ...result.files.map( - (e) => UniversalFile(data: e.xFile, type: UniversalFileType.image), + ...results.map( + (xfile) => UniversalFile(data: xfile, type: UniversalFileType.image), ), ]; } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index fbd2b84a..860b6397 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -19,19 +19,19 @@ PODS: - Firebase/Messaging (12.4.0): - Firebase/CoreOnly - FirebaseMessaging (~> 12.4.0) - - firebase_analytics (12.0.3): + - firebase_analytics (12.0.4): - firebase_core - FirebaseAnalytics (= 12.4.0) - FlutterMacOS - - firebase_core (4.2.0): + - firebase_core (4.2.1): - Firebase/CoreOnly (~> 12.4.0) - FlutterMacOS - - firebase_crashlytics (5.0.3): + - firebase_crashlytics (5.0.4): - Firebase/CoreOnly (~> 12.4.0) - Firebase/Crashlytics (~> 12.4.0) - firebase_core - FlutterMacOS - - firebase_messaging (16.0.3): + - firebase_messaging (16.0.4): - Firebase/CoreOnly (~> 12.4.0) - Firebase/Messaging (~> 12.4.0) - firebase_core @@ -416,10 +416,10 @@ SPEC CHECKSUMS: file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e - firebase_analytics: d876586269c1d8d2b3dcac085bc2d97c62abc9df - firebase_core: d81d1a44df95699ce074ae63d8cb43e9df21e142 - firebase_crashlytics: 723622cc39a9fa7320585424f5864c5699893ce1 - firebase_messaging: 31f412ae5a54e02d1c46d467969f7ad92c4b81ec + firebase_analytics: 09241c4796c1c42a02349ef8bf30025f5b640f0e + firebase_core: e054894ab56033ef9bcbe2d9eac9395e5306e2fc + firebase_crashlytics: c2438b5f5bdcacf59d0eaee5852c6b0ab09dab77 + firebase_messaging: 373ac3a56e5aa37bb9aff4127f700aa5973c1168 FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3 FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018 diff --git a/pubspec.lock b/pubspec.lock index 900d0c08..193f96e1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -282,7 +282,7 @@ packages: source: hosted version: "4.1.0" convert: - dependency: transitive + dependency: "direct main" description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 @@ -1185,10 +1185,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: d8f590a69729f719177ea68eb1e598295e8dbc41bbc247fed78b2c8a25660d7c + sha256: c92d18e1fe994cb06d48aa786c46b142a5633067e8297cff6b5a3ac742620104 url: "https://pub.dev" source: hosted - version: "16.3.0" + version: "17.0.0" google_fonts: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 8693145a..7a25b21a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,7 @@ dependencies: cupertino_icons: ^1.0.8 flutter_hooks: ^0.21.3+1 hooks_riverpod: ^2.6.1 - go_router: ^16.3.0 + go_router: ^17.0.0 styled_widget: ^0.4.1 shared_preferences: ^2.5.3 flutter_riverpod: ^2.6.1 @@ -165,6 +165,7 @@ dependencies: dio_smart_retry: ^7.0.1 flutter_expandable_fab: ^2.5.2 event_bus: ^2.0.1 + convert: ^3.1.2 dev_dependencies: flutter_test: