From 8fe3a664a6944201d106535df79634b6da17c2ee Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 2 Oct 2025 01:13:41 +0800 Subject: [PATCH] :recycle: Better file upload --- ios/Podfile.lock | 6 + lib/models/file.dart | 2 + lib/models/file.freezed.dart | 43 ++--- lib/models/file.g.dart | 2 + lib/pods/chat/chat_subscribe.g.dart | 2 +- lib/pods/chat/chat_summary.g.dart | 2 +- lib/pods/chat/messages_notifier.dart | 20 +-- lib/pods/chat/messages_notifier.g.dart | 2 +- lib/screens/account/me/profile_update.dart | 12 +- lib/screens/chat/chat.dart | 12 +- lib/screens/chat/room.dart | 17 +- lib/screens/creators/publishers.dart | 12 +- lib/screens/developers/edit_app.dart | 12 +- lib/screens/developers/edit_bot.dart | 12 +- lib/screens/realm/realms.dart | 12 +- lib/services/compose_storage_db.g.dart | 2 +- lib/services/file.dart | 179 +------------------- lib/services/file_uploader.dart | 161 +++++++++++++++++- lib/widgets/content/attachment_preview.dart | 76 ++++++--- lib/widgets/content/cloud_file_picker.dart | 21 +-- lib/widgets/post/compose_card.dart | 2 +- lib/widgets/post/compose_shared.dart | 41 +---- lib/widgets/share/share_sheet.dart | 26 +-- 23 files changed, 293 insertions(+), 383 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d641cf0e..506bff91 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,5 +1,7 @@ PODS: - Alamofire (5.10.2) + - app_links (6.4.1): + - Flutter - connectivity_plus (0.0.1): - Flutter - croppy (0.0.1): @@ -303,6 +305,7 @@ 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`) @@ -379,6 +382,8 @@ SPEC REPOS: - WebRTC-SDK EXTERNAL SOURCES: + app_links: + :path: ".symlinks/plugins/app_links/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" croppy: @@ -468,6 +473,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 + app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30 device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe diff --git a/lib/models/file.dart b/lib/models/file.dart index ca93b3d0..05caed89 100644 --- a/lib/models/file.dart +++ b/lib/models/file.dart @@ -14,6 +14,7 @@ sealed class UniversalFile with _$UniversalFile { required dynamic data, required UniversalFileType type, @Default(false) bool isLink, + String? displayName, }) = _UniversalFile; factory UniversalFile.fromJson(Map json) => @@ -31,6 +32,7 @@ sealed class UniversalFile with _$UniversalFile { 'video' => UniversalFileType.video, _ => UniversalFileType.file, }, + displayName: attachment.name, ); } } diff --git a/lib/models/file.freezed.dart b/lib/models/file.freezed.dart index 5dda227f..8cb8545d 100644 --- a/lib/models/file.freezed.dart +++ b/lib/models/file.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$UniversalFile { - dynamic get data; UniversalFileType get type; bool get isLink; + dynamic get data; UniversalFileType get type; bool get isLink; String? get displayName; /// Create a copy of UniversalFile /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,16 +28,16 @@ $UniversalFileCopyWith get copyWith => _$UniversalFileCopyWithImp @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is UniversalFile&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.type, type) || other.type == type)&&(identical(other.isLink, isLink) || other.isLink == isLink)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is UniversalFile&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.type, type) || other.type == type)&&(identical(other.isLink, isLink) || other.isLink == isLink)&&(identical(other.displayName, displayName) || other.displayName == displayName)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(data),type,isLink); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(data),type,isLink,displayName); @override String toString() { - return 'UniversalFile(data: $data, type: $type, isLink: $isLink)'; + return 'UniversalFile(data: $data, type: $type, isLink: $isLink, displayName: $displayName)'; } @@ -48,7 +48,7 @@ abstract mixin class $UniversalFileCopyWith<$Res> { factory $UniversalFileCopyWith(UniversalFile value, $Res Function(UniversalFile) _then) = _$UniversalFileCopyWithImpl; @useResult $Res call({ - dynamic data, UniversalFileType type, bool isLink + dynamic data, UniversalFileType type, bool isLink, String? displayName }); @@ -65,12 +65,13 @@ class _$UniversalFileCopyWithImpl<$Res> /// Create a copy of UniversalFile /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? data = freezed,Object? type = null,Object? isLink = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? data = freezed,Object? type = null,Object? isLink = null,Object? displayName = freezed,}) { return _then(_self.copyWith( data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable as dynamic,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as UniversalFileType,isLink: null == isLink ? _self.isLink : isLink // ignore: cast_nullable_to_non_nullable -as bool, +as bool,displayName: freezed == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable +as String?, )); } @@ -152,10 +153,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( dynamic data, UniversalFileType type, bool isLink)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( dynamic data, UniversalFileType type, bool isLink, String? displayName)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _UniversalFile() when $default != null: -return $default(_that.data,_that.type,_that.isLink);case _: +return $default(_that.data,_that.type,_that.isLink,_that.displayName);case _: return orElse(); } @@ -173,10 +174,10 @@ return $default(_that.data,_that.type,_that.isLink);case _: /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( dynamic data, UniversalFileType type, bool isLink) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( dynamic data, UniversalFileType type, bool isLink, String? displayName) $default,) {final _that = this; switch (_that) { case _UniversalFile(): -return $default(_that.data,_that.type,_that.isLink);} +return $default(_that.data,_that.type,_that.isLink,_that.displayName);} } /// A variant of `when` that fallback to returning `null` /// @@ -190,10 +191,10 @@ return $default(_that.data,_that.type,_that.isLink);} /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( dynamic data, UniversalFileType type, bool isLink)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( dynamic data, UniversalFileType type, bool isLink, String? displayName)? $default,) {final _that = this; switch (_that) { case _UniversalFile() when $default != null: -return $default(_that.data,_that.type,_that.isLink);case _: +return $default(_that.data,_that.type,_that.isLink,_that.displayName);case _: return null; } @@ -205,12 +206,13 @@ return $default(_that.data,_that.type,_that.isLink);case _: @JsonSerializable() class _UniversalFile extends UniversalFile { - const _UniversalFile({required this.data, required this.type, this.isLink = false}): super._(); + const _UniversalFile({required this.data, required this.type, this.isLink = false, this.displayName}): super._(); factory _UniversalFile.fromJson(Map json) => _$UniversalFileFromJson(json); @override final dynamic data; @override final UniversalFileType type; @override@JsonKey() final bool isLink; +@override final String? displayName; /// Create a copy of UniversalFile /// with the given fields replaced by the non-null parameter values. @@ -225,16 +227,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _UniversalFile&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.type, type) || other.type == type)&&(identical(other.isLink, isLink) || other.isLink == isLink)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _UniversalFile&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.type, type) || other.type == type)&&(identical(other.isLink, isLink) || other.isLink == isLink)&&(identical(other.displayName, displayName) || other.displayName == displayName)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(data),type,isLink); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(data),type,isLink,displayName); @override String toString() { - return 'UniversalFile(data: $data, type: $type, isLink: $isLink)'; + return 'UniversalFile(data: $data, type: $type, isLink: $isLink, displayName: $displayName)'; } @@ -245,7 +247,7 @@ abstract mixin class _$UniversalFileCopyWith<$Res> implements $UniversalFileCopy factory _$UniversalFileCopyWith(_UniversalFile value, $Res Function(_UniversalFile) _then) = __$UniversalFileCopyWithImpl; @override @useResult $Res call({ - dynamic data, UniversalFileType type, bool isLink + dynamic data, UniversalFileType type, bool isLink, String? displayName }); @@ -262,12 +264,13 @@ class __$UniversalFileCopyWithImpl<$Res> /// Create a copy of UniversalFile /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? data = freezed,Object? type = null,Object? isLink = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? data = freezed,Object? type = null,Object? isLink = null,Object? displayName = freezed,}) { return _then(_UniversalFile( data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable as dynamic,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as UniversalFileType,isLink: null == isLink ? _self.isLink : isLink // ignore: cast_nullable_to_non_nullable -as bool, +as bool,displayName: freezed == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable +as String?, )); } diff --git a/lib/models/file.g.dart b/lib/models/file.g.dart index 16cb2c3c..f92e7d77 100644 --- a/lib/models/file.g.dart +++ b/lib/models/file.g.dart @@ -11,6 +11,7 @@ _UniversalFile _$UniversalFileFromJson(Map json) => data: json['data'], type: $enumDecode(_$UniversalFileTypeEnumMap, json['type']), isLink: json['is_link'] as bool? ?? false, + displayName: json['display_name'] as String?, ); Map _$UniversalFileToJson(_UniversalFile instance) => @@ -18,6 +19,7 @@ Map _$UniversalFileToJson(_UniversalFile instance) => 'data': instance.data, 'type': _$UniversalFileTypeEnumMap[instance.type]!, 'is_link': instance.isLink, + 'display_name': instance.displayName, }; const _$UniversalFileTypeEnumMap = { diff --git a/lib/pods/chat/chat_subscribe.g.dart b/lib/pods/chat/chat_subscribe.g.dart index 1e31e484..27d24a4d 100644 --- a/lib/pods/chat/chat_subscribe.g.dart +++ b/lib/pods/chat/chat_subscribe.g.dart @@ -7,7 +7,7 @@ part of 'chat_subscribe.dart'; // ************************************************************************** String _$chatSubscribeNotifierHash() => - r'df65ecf15d0e97d7e6850ac57b4e681606e77179'; + r'c605e0c9c45df64e5ba7b65f8de9b47bde8e2b3b'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/pods/chat/chat_summary.g.dart b/lib/pods/chat/chat_summary.g.dart index b2ca0fe2..34a83065 100644 --- a/lib/pods/chat/chat_summary.g.dart +++ b/lib/pods/chat/chat_summary.g.dart @@ -6,7 +6,7 @@ part of 'chat_summary.dart'; // RiverpodGenerator // ************************************************************************** -String _$chatSummaryHash() => r'7b79dba7445f634373fbb2ee0ced99b2302097c2'; +String _$chatSummaryHash() => r'33815a3bd81d20902b7063e8194fe336930df9b4'; /// See also [ChatSummary]. @ProviderFor(ChatSummary) diff --git a/lib/pods/chat/messages_notifier.dart b/lib/pods/chat/messages_notifier.dart index 2c6fb0de..ab27497d 100644 --- a/lib/pods/chat/messages_notifier.dart +++ b/lib/pods/chat/messages_notifier.dart @@ -7,11 +7,10 @@ import "package:island/database/drift_db.dart"; import "package:island/database/message.dart"; import "package:island/models/chat.dart"; import "package:island/models/file.dart"; -import "package:island/pods/config.dart"; import "package:island/pods/database.dart"; import "package:island/pods/lifecycle.dart"; import "package:island/pods/network.dart"; -import "package:island/services/file.dart"; +import "package:island/services/file_uploader.dart"; import "package:island/talker.dart"; import "package:island/widgets/alert.dart"; import "package:riverpod_annotation/riverpod_annotation.dart"; @@ -362,9 +361,6 @@ class MessagesNotifier extends _$MessagesNotifier { }) async { final nonce = const Uuid().v4(); talker.log('Sending message with nonce $nonce'); - final baseUrl = ref.read(serverUrlProvider); - final token = await getToken(ref.watch(tokenProvider)); - if (token == null) throw ArgumentError('Access token is null'); final mockMessage = SnChatMessage( id: 'pending_$nonce', @@ -393,19 +389,9 @@ class MessagesNotifier extends _$MessagesNotifier { var cloudAttachments = List.empty(growable: true); for (var idx = 0; idx < attachments.length; idx++) { final cloudFile = - await putFileToCloud( + await FileUploader.createCloudFile( fileData: attachments[idx], - atk: token, - baseUrl: baseUrl, - filename: attachments[idx].data.name ?? 'Post media', - mimetype: - attachments[idx].data.mimeType ?? - switch (attachments[idx].type) { - UniversalFileType.image => 'image/unknown', - UniversalFileType.video => 'video/unknown', - UniversalFileType.audio => 'audio/unknown', - UniversalFileType.file => 'application/octet-stream', - }, + client: ref.read(apiClientProvider), onProgress: (progress, _) { _fileUploadProgress[localMessage.id]?[idx] = progress; onProgress?.call( diff --git a/lib/pods/chat/messages_notifier.g.dart b/lib/pods/chat/messages_notifier.g.dart index 03f6f334..4f5bb8fc 100644 --- a/lib/pods/chat/messages_notifier.g.dart +++ b/lib/pods/chat/messages_notifier.g.dart @@ -6,7 +6,7 @@ part of 'messages_notifier.dart'; // RiverpodGenerator // ************************************************************************** -String _$messagesNotifierHash() => r'3aad1491b777570913f3867abd280fa59949b1f1'; +String _$messagesNotifierHash() => r'b0cff44cea9f15a2684b602c48b32cd3d78875ab'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/screens/account/me/profile_update.dart b/lib/screens/account/me/profile_update.dart index 6a7241ad..1d74a884 100644 --- a/lib/screens/account/me/profile_update.dart +++ b/lib/screens/account/me/profile_update.dart @@ -9,10 +9,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:island/models/file.dart'; import 'package:island/models/account.dart'; -import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/services/file.dart'; +import 'package:island/services/file_uploader.dart'; import 'package:island/services/timezone.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; @@ -62,19 +62,13 @@ class UpdateProfileScreen extends HookConsumerWidget { submitting.value = true; try { - final baseUrl = ref.watch(serverUrlProvider); - final token = await getToken(ref.watch(tokenProvider)); - if (token == null) throw ArgumentError('Token is null'); final cloudFile = - await putFileToCloud( + await FileUploader.createCloudFile( + client: ref.read(apiClientProvider), fileData: UniversalFile( data: result, type: UniversalFileType.image, ), - atk: token, - baseUrl: baseUrl, - filename: result.name, - mimetype: result.mimeType ?? 'image/jpeg', ).future; if (cloudFile == null) { throw ArgumentError('Failed to upload the file...'); diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index 35b684dd..0c5f6303 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -13,10 +13,10 @@ import 'package:island/models/file.dart'; import 'package:island/models/realm.dart'; import 'package:island/pods/chat/call.dart'; import 'package:island/pods/chat/chat_summary.dart'; -import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/screens/realm/realms.dart'; import 'package:island/services/file.dart'; +import 'package:island/services/file_uploader.dart'; import 'package:island/services/responsive.dart'; import 'package:island/widgets/account/account_picker.dart'; import 'package:island/widgets/alert.dart'; @@ -644,19 +644,13 @@ class EditChatScreen extends HookConsumerWidget { submitting.value = true; try { - final baseUrl = ref.watch(serverUrlProvider); - final token = await getToken(ref.watch(tokenProvider)); - if (token == null) throw ArgumentError('Token is null'); final cloudFile = - await putFileToCloud( + await FileUploader.createCloudFile( + client: ref.read(apiClientProvider), fileData: UniversalFile( data: result, type: UniversalFileType.image, ), - atk: token, - baseUrl: baseUrl, - filename: result.name, - mimetype: result.mimeType ?? 'image/jpeg', ).future; if (cloudFile == null) { throw ArgumentError('Failed to upload the file...'); diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index a6529b92..e9ec7334 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -11,11 +11,10 @@ import "package:island/models/chat.dart"; import "package:island/models/file.dart"; import "package:island/pods/chat/chat_rooms.dart"; import "package:island/pods/chat/chat_subscribe.dart"; -import "package:island/pods/config.dart"; import "package:island/pods/chat/messages_notifier.dart"; import "package:island/pods/network.dart"; import "package:island/pods/chat/chat_online_count.dart"; -import "package:island/services/file.dart"; +import "package:island/services/file_uploader.dart"; import "package:island/screens/chat/chat.dart"; import "package:island/services/responsive.dart"; import "package:island/widgets/alert.dart"; @@ -24,7 +23,6 @@ import "package:island/widgets/attachment_uploader.dart"; import "package:island/widgets/chat/call_overlay.dart"; import "package:island/widgets/chat/message_item.dart"; import "package:island/widgets/content/cloud_files.dart"; -import "package:island/widgets/post/compose_shared.dart"; import "package:island/widgets/response.dart"; import "package:material_symbols_icons/material_symbols_icons.dart"; import "package:styled_widget/styled_widget.dart"; @@ -348,10 +346,6 @@ class ChatRoomScreen extends HookConsumerWidget { ); if (config == null) return; - final baseUrl = ref.watch(serverUrlProvider); - final token = await getToken(ref.watch(tokenProvider)); - if (token == null) throw ArgumentError('Token is null'); - try { // Use 'chat-upload' as temporary key for progress attachmentProgress.value = { @@ -360,15 +354,10 @@ class ChatRoomScreen extends HookConsumerWidget { }; final cloudFile = - await putFileToCloud( + await FileUploader.createCloudFile( + client: ref.read(apiClientProvider), fileData: attachment, - atk: token, - baseUrl: baseUrl, poolId: config.poolId, - filename: attachment.data.name ?? 'Chat media', - mimetype: - attachment.data.mimeType ?? - ComposeLogic.getMimeTypeFromFileType(attachment.type), mode: attachment.type == UniversalFileType.file ? FileUploadMode.generic diff --git a/lib/screens/creators/publishers.dart b/lib/screens/creators/publishers.dart index 890b369d..0b1eaad9 100644 --- a/lib/screens/creators/publishers.dart +++ b/lib/screens/creators/publishers.dart @@ -10,11 +10,11 @@ import 'package:image_picker/image_picker.dart'; import 'package:island/models/file.dart'; import 'package:island/models/publisher.dart'; import 'package:island/models/realm.dart'; -import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/screens/realm/realms.dart'; import 'package:island/services/file.dart'; +import 'package:island/services/file_uploader.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; @@ -94,19 +94,13 @@ class EditPublisherScreen extends HookConsumerWidget { submitting.value = true; try { - final baseUrl = ref.watch(serverUrlProvider); - final token = await getToken(ref.watch(tokenProvider)); - if (token == null) throw ArgumentError('Token is null'); final cloudFile = - await putFileToCloud( + await FileUploader.createCloudFile( fileData: UniversalFile( data: result, type: UniversalFileType.image, ), - atk: token, - baseUrl: baseUrl, - filename: result.name, - mimetype: result.mimeType ?? 'image/jpeg', + client: ref.read(apiClientProvider), ).future; if (cloudFile == null) { throw ArgumentError('Failed to upload the file...'); diff --git a/lib/screens/developers/edit_app.dart b/lib/screens/developers/edit_app.dart index fda47fae..709581b7 100644 --- a/lib/screens/developers/edit_app.dart +++ b/lib/screens/developers/edit_app.dart @@ -6,10 +6,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:island/models/custom_app.dart'; import 'package:island/models/file.dart'; -import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/screens/developers/apps.dart'; import 'package:island/services/file.dart'; +import 'package:island/services/file_uploader.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; @@ -137,19 +137,13 @@ class EditAppScreen extends HookConsumerWidget { submitting.value = true; try { - final baseUrl = ref.watch(serverUrlProvider); - final token = await getToken(ref.watch(tokenProvider)); - if (token == null) throw ArgumentError('Token is null'); final cloudFile = - await putFileToCloud( + await FileUploader.createCloudFile( + client: ref.read(apiClientProvider), fileData: UniversalFile( data: result, type: UniversalFileType.image, ), - atk: token, - baseUrl: baseUrl, - filename: result.name, - mimetype: result.mimeType ?? 'image/jpeg', ).future; if (cloudFile == null) { throw ArgumentError('Failed to upload the file...'); diff --git a/lib/screens/developers/edit_bot.dart b/lib/screens/developers/edit_bot.dart index 3350fafb..1bba7e7f 100644 --- a/lib/screens/developers/edit_bot.dart +++ b/lib/screens/developers/edit_bot.dart @@ -7,9 +7,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:island/models/bot.dart'; import 'package:island/models/file.dart'; -import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/services/file.dart'; +import 'package:island/services/file_uploader.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; @@ -123,19 +123,13 @@ class EditBotScreen extends HookConsumerWidget { submitting.value = true; try { - final baseUrl = ref.watch(serverUrlProvider); - final token = await getToken(ref.watch(tokenProvider)); - if (token == null) throw ArgumentError('Token is null'); final cloudFile = - await putFileToCloud( + await FileUploader.createCloudFile( fileData: UniversalFile( data: result, type: UniversalFileType.image, ), - atk: token, - baseUrl: baseUrl, - filename: result.name, - mimetype: result.mimeType ?? 'image/jpeg', + client: ref.read(apiClientProvider), ).future; if (cloudFile == null) { throw ArgumentError('Failed to upload the file...'); diff --git a/lib/screens/realm/realms.dart b/lib/screens/realm/realms.dart index 96a0372b..62f0f7f1 100644 --- a/lib/screens/realm/realms.dart +++ b/lib/screens/realm/realms.dart @@ -9,9 +9,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:island/models/file.dart'; import 'package:island/models/realm.dart'; -import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/services/file.dart'; +import 'package:island/services/file_uploader.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; @@ -204,19 +204,13 @@ class EditRealmScreen extends HookConsumerWidget { showLoadingModal(context); submitting.value = true; try { - final baseUrl = ref.watch(serverUrlProvider); - final token = await getToken(ref.watch(tokenProvider)); - if (token == null) throw ArgumentError('Access token is null'); final cloudFile = - await putFileToCloud( + await FileUploader.createCloudFile( + client: ref.read(apiClientProvider), fileData: UniversalFile( data: result, type: UniversalFileType.image, ), - atk: token, - baseUrl: baseUrl, - filename: result.name, - mimetype: result.mimeType ?? 'image/jpeg', ).future; if (cloudFile == null) { throw ArgumentError('Failed to upload the file...'); diff --git a/lib/services/compose_storage_db.g.dart b/lib/services/compose_storage_db.g.dart index 03b520bb..e40d3ca1 100644 --- a/lib/services/compose_storage_db.g.dart +++ b/lib/services/compose_storage_db.g.dart @@ -7,7 +7,7 @@ part of 'compose_storage_db.dart'; // ************************************************************************** String _$composeStorageNotifierHash() => - r'8baf17aa06b6f69641c20645ba8a3dfe01c97f8c'; + r'e78dbfd8dbaf728970985aaa2ac4df3575ddfcdf'; /// See also [ComposeStorageNotifier]. @ProviderFor(ComposeStorageNotifier) diff --git a/lib/services/file.dart b/lib/services/file.dart index 78469ec8..2b1a329f 100644 --- a/lib/services/file.dart +++ b/lib/services/file.dart @@ -2,14 +2,7 @@ import 'dart:async'; import 'dart:ui'; import 'package:croppy/croppy.dart'; import 'package:cross_file/cross_file.dart'; -import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:island/models/file.dart'; -import 'package:island/services/file_uploader.dart'; -import 'package:native_exif/native_exif.dart'; - -enum FileUploadMode { generic, mediaSafe } Future cropImage( BuildContext context, { @@ -17,9 +10,12 @@ Future cropImage( List? allowedAspectRatios, bool replacePath = true, }) async { + if (!context.mounted) return null; + final imageBytes = await image.readAsBytes(); + if (!context.mounted) return null; final result = await showMaterialImageCropper( context, - imageProvider: MemoryImage(await image.readAsBytes()), + imageProvider: MemoryImage(imageBytes), showLoadingIndicatorOnSubmit: true, allowedAspectRatios: allowedAspectRatios, ); @@ -38,170 +34,3 @@ Future cropImage( mimeType: image.mimeType, ); } - -Completer putFileToCloud({ - required UniversalFile fileData, - required String atk, - required String baseUrl, - String? poolId, - String? filename, - String? mimetype, - FileUploadMode? mode, - Function(double progress, Duration estimate)? onProgress, -}) { - final completer = Completer(); - - final effectiveMode = - mode ?? - (fileData.type == UniversalFileType.file - ? FileUploadMode.generic - : FileUploadMode.mediaSafe); - - if (effectiveMode == FileUploadMode.mediaSafe && - fileData.isOnDevice && - fileData.type == UniversalFileType.image) { - final data = fileData.data; - if (data is XFile && - !kIsWeb && - (defaultTargetPlatform == TargetPlatform.iOS || - defaultTargetPlatform == TargetPlatform.android)) { - Exif.fromPath(data.path) - .then((exif) async { - final gpsAttributes = { - 'GPSLatitude': '', - 'GPSLatitudeRef': '', - 'GPSLongitude': '', - 'GPSLongitudeRef': '', - 'GPSAltitude': '', - 'GPSAltitudeRef': '', - 'GPSTimeStamp': '', - 'GPSProcessingMethod': '', - 'GPSDateStamp': '', - }; - await exif.writeAttributes(gpsAttributes); - }) - .then( - (_) => _processUpload( - fileData, - atk, - baseUrl, - poolId, - filename, - mimetype, - onProgress, - completer, - ), - ) - .catchError((e) { - debugPrint('Error removing GPS EXIF data: $e'); - return _processUpload( - fileData, - atk, - baseUrl, - poolId, - filename, - mimetype, - onProgress, - completer, - ); - }); - - return completer; - } - } - - _processUpload( - fileData, - atk, - baseUrl, - poolId, - filename, - mimetype, - onProgress, - completer, - ); - return completer; -} - -// Helper method to process the upload after any EXIF processing -Completer _processUpload( - UniversalFile fileData, - String atk, - String baseUrl, - String? poolId, - String? filename, - String? mimetype, - Function(double progress, Duration estimate)? onProgress, - Completer completer, -) { - late XFile file; - String actualFilename = filename ?? 'randomly_file'; - String actualMimetype = mimetype ?? ''; - Uint8List? byteData; - - // Handle the data based on what's in the UniversalFile - final data = fileData.data; - - if (data is XFile) { - file = data; - actualFilename = filename ?? data.name; - actualMimetype = mimetype ?? data.mimeType ?? ''; - } else if (data is List || data is Uint8List) { - byteData = data is List ? Uint8List.fromList(data) : data; - actualFilename = filename ?? 'uploaded_file'; - actualMimetype = mimetype ?? 'application/octet-stream'; - if (mimetype == null) { - completer.completeError( - ArgumentError('Mimetype is required when providing raw bytes.'), - ); - return completer; - } - file = XFile.fromData(byteData!, mimeType: actualMimetype); - } else if (data is SnCloudFile) { - // If the file is already on the cloud, just return it - completer.complete(data); - return completer; - } else { - completer.completeError( - ArgumentError( - 'Invalid fileData type. Expected data to be XFile, List, Uint8List, or SnCloudFile.', - ), - ); - return completer; - } - - // Create Dio instance - final dio = Dio( - BaseOptions( - baseUrl: baseUrl, - headers: { - 'Authorization': 'AtField $atk', - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, - ), - ); - - final uploader = FileUploader(dio); - - // Call progress start - onProgress?.call(0.0, Duration.zero); - uploader - .uploadFile( - file: file, - fileName: actualFilename, - contentType: actualMimetype, - poolId: poolId, - ) - .then((result) { - // Call progress end - onProgress?.call(1.0, Duration.zero); - completer.complete(result); - }) - .catchError((e) { - completer.completeError(e); - throw e; - }); - - return completer; -} diff --git a/lib/services/file_uploader.dart b/lib/services/file_uploader.dart index 0c5aa2a3..886b2533 100644 --- a/lib/services/file_uploader.dart +++ b/lib/services/file_uploader.dart @@ -1,17 +1,19 @@ import 'dart:async'; import 'dart:typed_data'; - import 'package:cross_file/cross_file.dart'; import 'package:crypto/crypto.dart'; import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:island/models/file.dart'; import 'package:island/pods/network.dart'; +import 'package:mime/mime.dart'; +import 'package:native_exif/native_exif.dart'; class FileUploader { - final Dio _dio; + final Dio _client; - FileUploader(this._dio); + FileUploader(this._client); /// Calculates the MD5 hash of a file. Future _calculateFileHash(XFile file) async { @@ -34,7 +36,7 @@ class FileUploader { final hash = await _calculateFileHash(file); final fileSize = await file.length(); - final response = await _dio.post( + final response = await _client.post( '/drive/files/upload/create', data: { 'hash': hash, @@ -65,7 +67,7 @@ class FileUploader { ), }); - await _dio.post( + await _client.post( '/drive/files/upload/chunk/$taskId/$chunkIndex', data: formData, ); @@ -73,7 +75,7 @@ class FileUploader { /// Completes the upload and returns the CloudFile object. Future completeUpload(String taskId) async { - final response = await _dio.post('/drive/files/upload/complete/$taskId'); + final response = await _client.post('/drive/files/upload/complete/$taskId'); return SnCloudFile.fromJson(response.data); } @@ -146,8 +148,155 @@ class FileUploader { // Step 3: Complete upload return await completeUpload(taskId); } + + static Completer createCloudFile({ + required UniversalFile fileData, + required Dio client, + String? poolId, + FileUploadMode? mode, + Function(double progress, Duration estimate)? onProgress, + }) { + final completer = Completer(); + + final effectiveMode = + mode ?? + (fileData.type == UniversalFileType.file + ? FileUploadMode.generic + : FileUploadMode.mediaSafe); + + if (effectiveMode == FileUploadMode.mediaSafe && + fileData.isOnDevice && + fileData.type == UniversalFileType.image) { + final data = fileData.data; + if (data is XFile && + !kIsWeb && + (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.android)) { + Exif.fromPath(data.path) + .then((exif) async { + final gpsAttributes = { + 'GPSLatitude': '', + 'GPSLatitudeRef': '', + 'GPSLongitude': '', + 'GPSLongitudeRef': '', + 'GPSAltitude': '', + 'GPSAltitudeRef': '', + 'GPSTimeStamp': '', + 'GPSProcessingMethod': '', + 'GPSDateStamp': '', + }; + await exif.writeAttributes(gpsAttributes); + }) + .then( + (_) => _processUpload( + fileData, + client, + poolId, + onProgress, + completer, + ), + ) + .catchError((e) { + debugPrint('Error removing GPS EXIF data: $e'); + return _processUpload( + fileData, + client, + poolId, + onProgress, + completer, + ); + }); + + return completer; + } + } + + _processUpload(fileData, client, poolId, onProgress, completer); + return completer; + } + + // Helper method to process the upload + static Completer _processUpload( + UniversalFile fileData, + Dio client, + String? poolId, + Function(double progress, Duration estimate)? onProgress, + Completer completer, + ) { + String actualMimetype = getMimeType(fileData); + late XFile file; + String actualFilename = fileData.displayName ?? 'randomly_file'; + Uint8List? byteData; + + // Handle the data based on what's in the UniversalFile + final data = fileData.data; + + if (data is XFile) { + file = data; + actualFilename = fileData.displayName ?? data.name; + } else if (data is List || data is Uint8List) { + byteData = data is List ? Uint8List.fromList(data) : data; + actualFilename = fileData.displayName ?? 'uploaded_file'; + file = XFile.fromData(byteData!, mimeType: actualMimetype); + } else if (data is SnCloudFile) { + // If the file is already on the cloud, just return it + completer.complete(data); + return completer; + } else { + completer.completeError( + ArgumentError( + 'Invalid fileData type. Expected data to be XFile, List, Uint8List, or SnCloudFile.', + ), + ); + return completer; + } + + final uploader = FileUploader(client); + + // Call progress start + onProgress?.call(0.0, Duration.zero); + uploader + .uploadFile( + file: file, + fileName: actualFilename, + contentType: actualMimetype, + poolId: poolId, + ) + .then((result) { + // Call progress end + onProgress?.call(1.0, Duration.zero); + completer.complete(result); + }) + .catchError((e) { + completer.completeError(e); + throw e; + }); + + return completer; + } + + /// Gets the MIME type of a UniversalFile. + static String getMimeType(UniversalFile file) { + final data = file.data; + if (data is XFile) { + final mime = data.mimeType; + if (mime != null && mime.isNotEmpty) return mime; + final filename = file.displayName ?? data.name; + final detected = lookupMimeType(filename); + if (detected != null) return detected; + throw Exception('Cannot detect mime type for file: $filename'); + } else if (data is List || data is Uint8List) { + return 'application/octet-stream'; + } else if (data is SnCloudFile) { + return data.mimeType ?? 'application/octet-stream'; + } else { + throw ArgumentError('Invalid file data type'); + } + } } +enum FileUploadMode { generic, mediaSafe } + // Riverpod provider for the FileUploader service final fileUploaderProvider = Provider((ref) { final dio = ref.watch(apiClientProvider); diff --git a/lib/widgets/content/attachment_preview.dart b/lib/widgets/content/attachment_preview.dart index f7d174ac..676dc544 100644 --- a/lib/widgets/content/attachment_preview.dart +++ b/lib/widgets/content/attachment_preview.dart @@ -10,6 +10,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/file.dart'; import 'package:island/pods/network.dart'; import 'package:island/services/file.dart'; +import 'package:island/services/file_uploader.dart'; import 'package:island/utils/format.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/cloud_files.dart'; @@ -107,13 +108,23 @@ class AttachmentPreview extends HookConsumerWidget { static final GlobalKey _sensitiveSelectorKey = GlobalKey(); - Future _showRenameDialog(BuildContext context, WidgetRef ref) async { - final nameController = TextEditingController(text: item.data.name); + String _getDisplayName() { + return item.displayName ?? + (item.data is XFile + ? (item.data as XFile).name + : item.isOnCloud + ? item.data.name + : ''); + } + + Future _showRenameSheet(BuildContext context, WidgetRef ref) async { + final nameController = TextEditingController(text: _getDisplayName()); String? errorMessage; await showModalBottomSheet( context: context, isScrollControlled: true, + useRootNavigator: true, builder: (context) => SheetScaffold( heightFactor: 0.6, @@ -152,22 +163,32 @@ class AttachmentPreview extends HookConsumerWidget { return; } - try { - showLoadingModal(context); - final apiClient = ref.watch(apiClientProvider); - await apiClient.patch( - '/drive/files/${item.data.id}/name', - data: jsonEncode(newName), - ); - final newData = item.data; - newData.name = newName; - final updatedFile = item.copyWith(data: newData); - onUpdate?.call(item.copyWith(data: updatedFile)); + if (item.isOnCloud) { + try { + showLoadingModal(context); + final apiClient = ref.watch(apiClientProvider); + await apiClient.patch( + '/drive/files/${item.data.id}/name', + data: jsonEncode(newName), + ); + final newData = item.data; + newData.name = newName; + onUpdate?.call( + item.copyWith( + data: newData, + displayName: newName, + ), + ); + if (context.mounted) Navigator.pop(context); + } catch (err) { + showErrorAlert(err); + } finally { + if (context.mounted) hideLoadingModal(context); + } + } else { + // Local file rename + onUpdate?.call(item.copyWith(displayName: newName)); if (context.mounted) Navigator.pop(context); - } catch (err) { - showErrorAlert(err); - } finally { - if (context.mounted) hideLoadingModal(context); } }, child: Text('rename'.tr()), @@ -292,6 +313,8 @@ class AttachmentPreview extends HookConsumerWidget { _ => Symbols.insert_drive_file, }; + final mimeType = FileUploader.getMimeType(item); + if (item.isOnCloud) { return CloudFileWidget(item: item.data); } else if (item.data is XFile) { @@ -321,7 +344,12 @@ class AttachmentPreview extends HookConsumerWidget { children: [ Icon(fallbackIcon), const Gap(6), - Text(file.name, textAlign: TextAlign.center), + Text( + _getDisplayName(), + textAlign: TextAlign.center, + ), + Text(mimeType, style: TextStyle(fontSize: 10)), + const Gap(1), FutureBuilder( future: file.length(), builder: (context, snapshot) { @@ -347,6 +375,8 @@ class AttachmentPreview extends HookConsumerWidget { children: [ Icon(fallbackIcon), const Gap(6), + Text(mimeType, style: TextStyle(fontSize: 10)), + const Gap(1), Text( formatFileSize(item.data.length), ).fontSize(11), @@ -542,12 +572,20 @@ class AttachmentPreview extends HookConsumerWidget { onUpdate?.call(item.copyWith(data: result)); }, ), + if (item.isOnDevice) + MenuAction( + title: 'rename'.tr(), + image: MenuImage.icon(Symbols.edit), + callback: () async { + await _showRenameSheet(context, ref); + }, + ), if (item.isOnCloud) MenuAction( title: 'rename'.tr(), image: MenuImage.icon(Symbols.edit), callback: () async { - await _showRenameDialog(context, ref); + await _showRenameSheet(context, ref); }, ), if (item.isOnCloud) diff --git a/lib/widgets/content/cloud_file_picker.dart b/lib/widgets/content/cloud_file_picker.dart index 0101855b..fd37189b 100644 --- a/lib/widgets/content/cloud_file_picker.dart +++ b/lib/widgets/content/cloud_file_picker.dart @@ -6,9 +6,8 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:island/models/file.dart'; -import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; -import 'package:island/services/file.dart'; +import 'package:island/services/file_uploader.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/attachment_preview.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -42,10 +41,6 @@ class CloudFilePicker extends HookConsumerWidget { Future startUpload() async { if (files.value.isEmpty) return; - final baseUrl = ref.read(serverUrlProvider); - final token = await getToken(ref.watch(tokenProvider)); - if (token == null) throw Exception("Unauthorized"); - List result = List.empty(growable: true); uploadProgress.value = 0; @@ -55,19 +50,9 @@ class CloudFilePicker extends HookConsumerWidget { uploadPosition.value = idx; final file = files.value[idx]; final cloudFile = - await putFileToCloud( + await FileUploader.createCloudFile( fileData: file, - atk: token, - baseUrl: baseUrl, - filename: file.data.name ?? 'Post media', - mimetype: - file.data.mimeType ?? - switch (file.type) { - UniversalFileType.image => 'image/unknown', - UniversalFileType.video => 'video/unknown', - UniversalFileType.audio => 'audio/unknown', - UniversalFileType.file => 'application/octet-stream', - }, + client: ref.read(apiClientProvider), onProgress: (progress, _) { uploadProgress.value = progress; }, diff --git a/lib/widgets/post/compose_card.dart b/lib/widgets/post/compose_card.dart index 9113d511..cbb566e8 100644 --- a/lib/widgets/post/compose_card.dart +++ b/lib/widgets/post/compose_card.dart @@ -32,7 +32,7 @@ class PostComposeCard extends HookConsumerWidget { final Function(SnPost)? onSubmit; final Function(ComposeState)? onStateChanged; - PostComposeCard({ + const PostComposeCard({ super.key, this.originalPost, this.initialState, diff --git a/lib/widgets/post/compose_shared.dart b/lib/widgets/post/compose_shared.dart index b301d716..f2836000 100644 --- a/lib/widgets/post/compose_shared.dart +++ b/lib/widgets/post/compose_shared.dart @@ -14,9 +14,8 @@ import 'package:island/models/post.dart'; import 'package:island/models/post_category.dart'; import 'package:island/models/publisher.dart'; import 'package:island/models/realm.dart'; -import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; -import 'package:island/services/file.dart'; +import 'package:island/services/file_uploader.dart'; import 'package:island/services/compose_storage_db.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/post/compose_link_attachments.dart'; @@ -177,25 +176,14 @@ class ComposeLogic { try { // Upload any local attachments first - final baseUrl = ref.watch(serverUrlProvider); - final token = await getToken(ref.watch(tokenProvider)); - if (token == null) throw ArgumentError('Token is null'); - for (int i = 0; i < state.attachments.value.length; i++) { final attachment = state.attachments.value[i]; if (attachment.data is! SnCloudFile) { try { final cloudFile = - await putFileToCloud( + await FileUploader.createCloudFile( + client: ref.read(apiClientProvider), fileData: attachment, - atk: token, - baseUrl: baseUrl, - filename: - attachment.data.name ?? - (state.postType == 1 ? 'Article media' : 'Post media'), - mimetype: - attachment.data.mimeType ?? - ComposeLogic.getMimeTypeFromFileType(attachment.type), ).future; if (cloudFile != null) { // Update attachments list with cloud file @@ -509,15 +497,11 @@ class ComposeLogic { WidgetRef ref, ComposeState state, int index, { - String? poolId, // For Unit Test + String? poolId, }) async { final attachment = state.attachments.value[index]; if (attachment.isOnCloud) return; - final baseUrl = ref.watch(serverUrlProvider); - final token = await getToken(ref.watch(tokenProvider)); - if (token == null) throw ArgumentError('Token is null'); - try { state.attachmentProgress.value = { ...state.attachmentProgress.value, @@ -530,19 +514,10 @@ class ComposeLogic { final selectedPoolId = resolveDefaultPoolId(ref, pools); cloudFile = - await putFileToCloud( + await FileUploader.createCloudFile( + client: ref.read(apiClientProvider), fileData: attachment, - atk: token, - baseUrl: baseUrl, - poolId: selectedPoolId, - filename: - attachment.data.name ?? - (attachment.type == UniversalFileType.file - ? 'General file' - : 'Post media'), - mimetype: - attachment.data.mimeType ?? - getMimeTypeFromFileType(attachment.type), + poolId: poolId ?? selectedPoolId, mode: attachment.type == UniversalFileType.file ? FileUploadMode.generic @@ -563,7 +538,7 @@ class ComposeLogic { clone[index] = UniversalFile(data: cloudFile, type: attachment.type); state.attachments.value = clone; } catch (err) { - showErrorAlert(err.toString()); + showErrorAlert(err); } finally { state.attachmentProgress.value = {...state.attachmentProgress.value} ..remove(index); diff --git a/lib/widgets/share/share_sheet.dart b/lib/widgets/share/share_sheet.dart index 96b6be82..a48a93b7 100644 --- a/lib/widgets/share/share_sheet.dart +++ b/lib/widgets/share/share_sheet.dart @@ -1,8 +1,10 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/services/file_uploader.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/sheet.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -10,11 +12,7 @@ import 'package:island/screens/posts/compose.dart'; import 'package:island/models/file.dart'; import 'package:island/pods/link_preview.dart'; import 'package:island/pods/network.dart'; -import 'package:island/pods/config.dart'; -import 'package:island/services/file.dart'; import 'package:mime/mime.dart'; - -import 'dart:io'; import 'package:path/path.dart' as path; import 'package:island/models/chat.dart'; import 'package:island/screens/chat/chat.dart'; @@ -192,7 +190,6 @@ class _ShareSheetState extends ConsumerState { setState(() => _isLoading = true); try { final apiClient = ref.read(apiClientProvider); - final serverUrl = ref.read(serverUrlProvider); String content = _messageController.text.trim(); List attachmentIds = []; @@ -216,11 +213,6 @@ class _ShareSheetState extends ConsumerState { case ShareContentType.file: // Upload files to cloud storage if (widget.content.files?.isNotEmpty == true) { - final token = ref.watch(tokenProvider)?.token; - if (token == null) { - throw Exception('Authentication required'); - } - final universalFiles = widget.content.files!.map((file) { UniversalFileType fileType; @@ -247,19 +239,9 @@ class _ShareSheetState extends ConsumerState { for (var idx = 0; idx < universalFiles.length; idx++) { final file = universalFiles[idx]; final cloudFile = - await putFileToCloud( + await FileUploader.createCloudFile( + client: apiClient, fileData: file, - atk: token, - baseUrl: serverUrl, - filename: file.data.name ?? 'Shared file', - mimetype: - file.data.mimeType ?? - switch (file.type) { - UniversalFileType.image => 'image/unknown', - UniversalFileType.video => 'video/unknown', - UniversalFileType.audio => 'audio/unknown', - UniversalFileType.file => 'application/octet-stream', - }, onProgress: (progress, _) { if (mounted) { setState(() {