♻️ Better file upload
This commit is contained in:
		@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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<String, dynamic> json) =>
 | 
			
		||||
@@ -31,6 +32,7 @@ sealed class UniversalFile with _$UniversalFile {
 | 
			
		||||
        'video' => UniversalFileType.video,
 | 
			
		||||
        _ => UniversalFileType.file,
 | 
			
		||||
      },
 | 
			
		||||
      displayName: attachment.name,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ T _$identity<T>(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<UniversalFile> 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 extends Object?>(TResult Function( dynamic data,  UniversalFileType type,  bool isLink)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
			
		||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(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 extends Object?>(TResult Function( dynamic data,  UniversalFileType type,  bool isLink)  $default,) {final _that = this;
 | 
			
		||||
@optionalTypeArgs TResult when<TResult extends Object?>(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 extends Object?>(TResult? Function( dynamic data,  UniversalFileType type,  bool isLink)?  $default,) {final _that = this;
 | 
			
		||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(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<String, dynamic> 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<String, dynamic> 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?,
 | 
			
		||||
  ));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ _UniversalFile _$UniversalFileFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      data: json['data'],
 | 
			
		||||
      type: $enumDecode(_$UniversalFileTypeEnumMap, json['type']),
 | 
			
		||||
      isLink: json['is_link'] as bool? ?? false,
 | 
			
		||||
      displayName: json['display_name'] as String?,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
Map<String, dynamic> _$UniversalFileToJson(_UniversalFile instance) =>
 | 
			
		||||
@@ -18,6 +19,7 @@ Map<String, dynamic> _$UniversalFileToJson(_UniversalFile instance) =>
 | 
			
		||||
      'data': instance.data,
 | 
			
		||||
      'type': _$UniversalFileTypeEnumMap[instance.type]!,
 | 
			
		||||
      'is_link': instance.isLink,
 | 
			
		||||
      'display_name': instance.displayName,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
const _$UniversalFileTypeEnumMap = {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ part of 'chat_subscribe.dart';
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
 | 
			
		||||
String _$chatSubscribeNotifierHash() =>
 | 
			
		||||
    r'df65ecf15d0e97d7e6850ac57b4e681606e77179';
 | 
			
		||||
    r'c605e0c9c45df64e5ba7b65f8de9b47bde8e2b3b';
 | 
			
		||||
 | 
			
		||||
/// Copied from Dart SDK
 | 
			
		||||
class _SystemHash {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ part of 'chat_summary.dart';
 | 
			
		||||
// RiverpodGenerator
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
 | 
			
		||||
String _$chatSummaryHash() => r'7b79dba7445f634373fbb2ee0ced99b2302097c2';
 | 
			
		||||
String _$chatSummaryHash() => r'33815a3bd81d20902b7063e8194fe336930df9b4';
 | 
			
		||||
 | 
			
		||||
/// See also [ChatSummary].
 | 
			
		||||
@ProviderFor(ChatSummary)
 | 
			
		||||
 
 | 
			
		||||
@@ -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(
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
 | 
			
		||||
// RiverpodGenerator
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
 | 
			
		||||
String _$messagesNotifierHash() => r'3aad1491b777570913f3867abd280fa59949b1f1';
 | 
			
		||||
String _$messagesNotifierHash() => r'b0cff44cea9f15a2684b602c48b32cd3d78875ab';
 | 
			
		||||
 | 
			
		||||
/// Copied from Dart SDK
 | 
			
		||||
class _SystemHash {
 | 
			
		||||
 
 | 
			
		||||
@@ -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...');
 | 
			
		||||
 
 | 
			
		||||
@@ -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...');
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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...');
 | 
			
		||||
 
 | 
			
		||||
@@ -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...');
 | 
			
		||||
 
 | 
			
		||||
@@ -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...');
 | 
			
		||||
 
 | 
			
		||||
@@ -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...');
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ part of 'compose_storage_db.dart';
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
 | 
			
		||||
String _$composeStorageNotifierHash() =>
 | 
			
		||||
    r'8baf17aa06b6f69641c20645ba8a3dfe01c97f8c';
 | 
			
		||||
    r'e78dbfd8dbaf728970985aaa2ac4df3575ddfcdf';
 | 
			
		||||
 | 
			
		||||
/// See also [ComposeStorageNotifier].
 | 
			
		||||
@ProviderFor(ComposeStorageNotifier)
 | 
			
		||||
 
 | 
			
		||||
@@ -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<XFile?> cropImage(
 | 
			
		||||
  BuildContext context, {
 | 
			
		||||
@@ -17,9 +10,12 @@ Future<XFile?> cropImage(
 | 
			
		||||
  List<CropAspectRatio?>? 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<XFile?> cropImage(
 | 
			
		||||
    mimeType: image.mimeType,
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Completer<SnCloudFile?> 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<SnCloudFile?>();
 | 
			
		||||
 | 
			
		||||
  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<SnCloudFile?> _processUpload(
 | 
			
		||||
  UniversalFile fileData,
 | 
			
		||||
  String atk,
 | 
			
		||||
  String baseUrl,
 | 
			
		||||
  String? poolId,
 | 
			
		||||
  String? filename,
 | 
			
		||||
  String? mimetype,
 | 
			
		||||
  Function(double progress, Duration estimate)? onProgress,
 | 
			
		||||
  Completer<SnCloudFile?> 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<int> || data is Uint8List) {
 | 
			
		||||
    byteData = data is List<int> ? 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<int>, 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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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<String> _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<SnCloudFile> 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<SnCloudFile?> createCloudFile({
 | 
			
		||||
    required UniversalFile fileData,
 | 
			
		||||
    required Dio client,
 | 
			
		||||
    String? poolId,
 | 
			
		||||
    FileUploadMode? mode,
 | 
			
		||||
    Function(double progress, Duration estimate)? onProgress,
 | 
			
		||||
  }) {
 | 
			
		||||
    final completer = Completer<SnCloudFile?>();
 | 
			
		||||
 | 
			
		||||
    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<SnCloudFile?> _processUpload(
 | 
			
		||||
    UniversalFile fileData,
 | 
			
		||||
    Dio client,
 | 
			
		||||
    String? poolId,
 | 
			
		||||
    Function(double progress, Duration estimate)? onProgress,
 | 
			
		||||
    Completer<SnCloudFile?> 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<int> || data is Uint8List) {
 | 
			
		||||
      byteData = data is List<int> ? 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<int>, 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<int> || 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<FileUploader>((ref) {
 | 
			
		||||
  final dio = ref.watch(apiClientProvider);
 | 
			
		||||
 
 | 
			
		||||
@@ -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<SensitiveMarksSelectorState> _sensitiveSelectorKey =
 | 
			
		||||
      GlobalKey<SensitiveMarksSelectorState>();
 | 
			
		||||
 | 
			
		||||
  Future<void> _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<void> _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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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<void> 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<SnCloudFile> 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;
 | 
			
		||||
                },
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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<ShareSheet> {
 | 
			
		||||
    setState(() => _isLoading = true);
 | 
			
		||||
    try {
 | 
			
		||||
      final apiClient = ref.read(apiClientProvider);
 | 
			
		||||
      final serverUrl = ref.read(serverUrlProvider);
 | 
			
		||||
 | 
			
		||||
      String content = _messageController.text.trim();
 | 
			
		||||
      List<String> attachmentIds = [];
 | 
			
		||||
@@ -216,11 +213,6 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
 | 
			
		||||
        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<ShareSheet> {
 | 
			
		||||
            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(() {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user