♻️ Better file upload

This commit is contained in:
2025-10-02 01:13:41 +08:00
parent 3bfc0b8181
commit 8fe3a664a6
23 changed files with 293 additions and 383 deletions

View File

@@ -1,5 +1,7 @@
PODS: PODS:
- Alamofire (5.10.2) - Alamofire (5.10.2)
- app_links (6.4.1):
- Flutter
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- croppy (0.0.1): - croppy (0.0.1):
@@ -303,6 +305,7 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- Alamofire - Alamofire
- app_links (from `.symlinks/plugins/app_links/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- croppy (from `.symlinks/plugins/croppy/ios`) - croppy (from `.symlinks/plugins/croppy/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
@@ -379,6 +382,8 @@ SPEC REPOS:
- WebRTC-SDK - WebRTC-SDK
EXTERNAL SOURCES: EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
connectivity_plus: connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios" :path: ".symlinks/plugins/connectivity_plus/ios"
croppy: croppy:
@@ -468,6 +473,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30 croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe

View File

@@ -14,6 +14,7 @@ sealed class UniversalFile with _$UniversalFile {
required dynamic data, required dynamic data,
required UniversalFileType type, required UniversalFileType type,
@Default(false) bool isLink, @Default(false) bool isLink,
String? displayName,
}) = _UniversalFile; }) = _UniversalFile;
factory UniversalFile.fromJson(Map<String, dynamic> json) => factory UniversalFile.fromJson(Map<String, dynamic> json) =>
@@ -31,6 +32,7 @@ sealed class UniversalFile with _$UniversalFile {
'video' => UniversalFileType.video, 'video' => UniversalFileType.video,
_ => UniversalFileType.file, _ => UniversalFileType.file,
}, },
displayName: attachment.name,
); );
} }
} }

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$UniversalFile { 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 /// Create a copy of UniversalFile
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $UniversalFileCopyWith<UniversalFile> get copyWith => _$UniversalFileCopyWithImp
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory $UniversalFileCopyWith(UniversalFile value, $Res Function(UniversalFile) _then) = _$UniversalFileCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// Create a copy of UniversalFile
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_self.copyWith(
data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable 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 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 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) { switch (_that) {
case _UniversalFile() when $default != null: 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(); 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) { switch (_that) {
case _UniversalFile(): 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` /// 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) { switch (_that) {
case _UniversalFile() when $default != null: 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; return null;
} }
@@ -205,12 +206,13 @@ return $default(_that.data,_that.type,_that.isLink);case _:
@JsonSerializable() @JsonSerializable()
class _UniversalFile extends UniversalFile { 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); factory _UniversalFile.fromJson(Map<String, dynamic> json) => _$UniversalFileFromJson(json);
@override final dynamic data; @override final dynamic data;
@override final UniversalFileType type; @override final UniversalFileType type;
@override@JsonKey() final bool isLink; @override@JsonKey() final bool isLink;
@override final String? displayName;
/// Create a copy of UniversalFile /// Create a copy of UniversalFile
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -225,16 +227,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory _$UniversalFileCopyWith(_UniversalFile value, $Res Function(_UniversalFile) _then) = __$UniversalFileCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// Create a copy of UniversalFile
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_UniversalFile(
data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable 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 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 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?,
)); ));
} }

View File

@@ -11,6 +11,7 @@ _UniversalFile _$UniversalFileFromJson(Map<String, dynamic> json) =>
data: json['data'], data: json['data'],
type: $enumDecode(_$UniversalFileTypeEnumMap, json['type']), type: $enumDecode(_$UniversalFileTypeEnumMap, json['type']),
isLink: json['is_link'] as bool? ?? false, isLink: json['is_link'] as bool? ?? false,
displayName: json['display_name'] as String?,
); );
Map<String, dynamic> _$UniversalFileToJson(_UniversalFile instance) => Map<String, dynamic> _$UniversalFileToJson(_UniversalFile instance) =>
@@ -18,6 +19,7 @@ Map<String, dynamic> _$UniversalFileToJson(_UniversalFile instance) =>
'data': instance.data, 'data': instance.data,
'type': _$UniversalFileTypeEnumMap[instance.type]!, 'type': _$UniversalFileTypeEnumMap[instance.type]!,
'is_link': instance.isLink, 'is_link': instance.isLink,
'display_name': instance.displayName,
}; };
const _$UniversalFileTypeEnumMap = { const _$UniversalFileTypeEnumMap = {

View File

@@ -7,7 +7,7 @@ part of 'chat_subscribe.dart';
// ************************************************************************** // **************************************************************************
String _$chatSubscribeNotifierHash() => String _$chatSubscribeNotifierHash() =>
r'df65ecf15d0e97d7e6850ac57b4e681606e77179'; r'c605e0c9c45df64e5ba7b65f8de9b47bde8e2b3b';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {

View File

@@ -6,7 +6,7 @@ part of 'chat_summary.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$chatSummaryHash() => r'7b79dba7445f634373fbb2ee0ced99b2302097c2'; String _$chatSummaryHash() => r'33815a3bd81d20902b7063e8194fe336930df9b4';
/// See also [ChatSummary]. /// See also [ChatSummary].
@ProviderFor(ChatSummary) @ProviderFor(ChatSummary)

View File

@@ -7,11 +7,10 @@ import "package:island/database/drift_db.dart";
import "package:island/database/message.dart"; import "package:island/database/message.dart";
import "package:island/models/chat.dart"; import "package:island/models/chat.dart";
import "package:island/models/file.dart"; import "package:island/models/file.dart";
import "package:island/pods/config.dart";
import "package:island/pods/database.dart"; import "package:island/pods/database.dart";
import "package:island/pods/lifecycle.dart"; import "package:island/pods/lifecycle.dart";
import "package:island/pods/network.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/talker.dart";
import "package:island/widgets/alert.dart"; import "package:island/widgets/alert.dart";
import "package:riverpod_annotation/riverpod_annotation.dart"; import "package:riverpod_annotation/riverpod_annotation.dart";
@@ -362,9 +361,6 @@ class MessagesNotifier extends _$MessagesNotifier {
}) async { }) async {
final nonce = const Uuid().v4(); final nonce = const Uuid().v4();
talker.log('Sending message with nonce $nonce'); talker.log('Sending message with nonce $nonce');
final baseUrl = ref.read(serverUrlProvider);
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Access token is null');
final mockMessage = SnChatMessage( final mockMessage = SnChatMessage(
id: 'pending_$nonce', id: 'pending_$nonce',
@@ -393,19 +389,9 @@ class MessagesNotifier extends _$MessagesNotifier {
var cloudAttachments = List.empty(growable: true); var cloudAttachments = List.empty(growable: true);
for (var idx = 0; idx < attachments.length; idx++) { for (var idx = 0; idx < attachments.length; idx++) {
final cloudFile = final cloudFile =
await putFileToCloud( await FileUploader.createCloudFile(
fileData: attachments[idx], fileData: attachments[idx],
atk: token, client: ref.read(apiClientProvider),
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',
},
onProgress: (progress, _) { onProgress: (progress, _) {
_fileUploadProgress[localMessage.id]?[idx] = progress; _fileUploadProgress[localMessage.id]?[idx] = progress;
onProgress?.call( onProgress?.call(

View File

@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$messagesNotifierHash() => r'3aad1491b777570913f3867abd280fa59949b1f1'; String _$messagesNotifierHash() => r'b0cff44cea9f15a2684b602c48b32cd3d78875ab';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {

View File

@@ -9,10 +9,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/account.dart'; import 'package:island/models/account.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/services/file.dart'; import 'package:island/services/file.dart';
import 'package:island/services/file_uploader.dart';
import 'package:island/services/timezone.dart'; import 'package:island/services/timezone.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
@@ -62,19 +62,13 @@ class UpdateProfileScreen extends HookConsumerWidget {
submitting.value = true; submitting.value = true;
try { try {
final baseUrl = ref.watch(serverUrlProvider);
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null');
final cloudFile = final cloudFile =
await putFileToCloud( await FileUploader.createCloudFile(
client: ref.read(apiClientProvider),
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,
), ),
atk: token,
baseUrl: baseUrl,
filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg',
).future; ).future;
if (cloudFile == null) { if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...'); throw ArgumentError('Failed to upload the file...');

View File

@@ -13,10 +13,10 @@ import 'package:island/models/file.dart';
import 'package:island/models/realm.dart'; import 'package:island/models/realm.dart';
import 'package:island/pods/chat/call.dart'; import 'package:island/pods/chat/call.dart';
import 'package:island/pods/chat/chat_summary.dart'; import 'package:island/pods/chat/chat_summary.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/screens/realm/realms.dart'; import 'package:island/screens/realm/realms.dart';
import 'package:island/services/file.dart'; import 'package:island/services/file.dart';
import 'package:island/services/file_uploader.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_picker.dart'; import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
@@ -644,19 +644,13 @@ class EditChatScreen extends HookConsumerWidget {
submitting.value = true; submitting.value = true;
try { try {
final baseUrl = ref.watch(serverUrlProvider);
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null');
final cloudFile = final cloudFile =
await putFileToCloud( await FileUploader.createCloudFile(
client: ref.read(apiClientProvider),
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,
), ),
atk: token,
baseUrl: baseUrl,
filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg',
).future; ).future;
if (cloudFile == null) { if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...'); throw ArgumentError('Failed to upload the file...');

View File

@@ -11,11 +11,10 @@ import "package:island/models/chat.dart";
import "package:island/models/file.dart"; import "package:island/models/file.dart";
import "package:island/pods/chat/chat_rooms.dart"; import "package:island/pods/chat/chat_rooms.dart";
import "package:island/pods/chat/chat_subscribe.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/chat/messages_notifier.dart";
import "package:island/pods/network.dart"; import "package:island/pods/network.dart";
import "package:island/pods/chat/chat_online_count.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/screens/chat/chat.dart";
import "package:island/services/responsive.dart"; import "package:island/services/responsive.dart";
import "package:island/widgets/alert.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/call_overlay.dart";
import "package:island/widgets/chat/message_item.dart"; import "package:island/widgets/chat/message_item.dart";
import "package:island/widgets/content/cloud_files.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:island/widgets/response.dart";
import "package:material_symbols_icons/material_symbols_icons.dart"; import "package:material_symbols_icons/material_symbols_icons.dart";
import "package:styled_widget/styled_widget.dart"; import "package:styled_widget/styled_widget.dart";
@@ -348,10 +346,6 @@ class ChatRoomScreen extends HookConsumerWidget {
); );
if (config == null) return; 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 { try {
// Use 'chat-upload' as temporary key for progress // Use 'chat-upload' as temporary key for progress
attachmentProgress.value = { attachmentProgress.value = {
@@ -360,15 +354,10 @@ class ChatRoomScreen extends HookConsumerWidget {
}; };
final cloudFile = final cloudFile =
await putFileToCloud( await FileUploader.createCloudFile(
client: ref.read(apiClientProvider),
fileData: attachment, fileData: attachment,
atk: token,
baseUrl: baseUrl,
poolId: config.poolId, poolId: config.poolId,
filename: attachment.data.name ?? 'Chat media',
mimetype:
attachment.data.mimeType ??
ComposeLogic.getMimeTypeFromFileType(attachment.type),
mode: mode:
attachment.type == UniversalFileType.file attachment.type == UniversalFileType.file
? FileUploadMode.generic ? FileUploadMode.generic

View File

@@ -10,11 +10,11 @@ import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/publisher.dart'; import 'package:island/models/publisher.dart';
import 'package:island/models/realm.dart'; import 'package:island/models/realm.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/screens/realm/realms.dart'; import 'package:island/screens/realm/realms.dart';
import 'package:island/services/file.dart'; import 'package:island/services/file.dart';
import 'package:island/services/file_uploader.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
@@ -94,19 +94,13 @@ class EditPublisherScreen extends HookConsumerWidget {
submitting.value = true; submitting.value = true;
try { try {
final baseUrl = ref.watch(serverUrlProvider);
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null');
final cloudFile = final cloudFile =
await putFileToCloud( await FileUploader.createCloudFile(
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,
), ),
atk: token, client: ref.read(apiClientProvider),
baseUrl: baseUrl,
filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg',
).future; ).future;
if (cloudFile == null) { if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...'); throw ArgumentError('Failed to upload the file...');

View File

@@ -6,10 +6,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:island/models/custom_app.dart'; import 'package:island/models/custom_app.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/screens/developers/apps.dart'; import 'package:island/screens/developers/apps.dart';
import 'package:island/services/file.dart'; import 'package:island/services/file.dart';
import 'package:island/services/file_uploader.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
@@ -137,19 +137,13 @@ class EditAppScreen extends HookConsumerWidget {
submitting.value = true; submitting.value = true;
try { try {
final baseUrl = ref.watch(serverUrlProvider);
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null');
final cloudFile = final cloudFile =
await putFileToCloud( await FileUploader.createCloudFile(
client: ref.read(apiClientProvider),
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,
), ),
atk: token,
baseUrl: baseUrl,
filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg',
).future; ).future;
if (cloudFile == null) { if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...'); throw ArgumentError('Failed to upload the file...');

View File

@@ -7,9 +7,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:island/models/bot.dart'; import 'package:island/models/bot.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/services/file.dart'; import 'package:island/services/file.dart';
import 'package:island/services/file_uploader.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
@@ -123,19 +123,13 @@ class EditBotScreen extends HookConsumerWidget {
submitting.value = true; submitting.value = true;
try { try {
final baseUrl = ref.watch(serverUrlProvider);
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null');
final cloudFile = final cloudFile =
await putFileToCloud( await FileUploader.createCloudFile(
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,
), ),
atk: token, client: ref.read(apiClientProvider),
baseUrl: baseUrl,
filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg',
).future; ).future;
if (cloudFile == null) { if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...'); throw ArgumentError('Failed to upload the file...');

View File

@@ -9,9 +9,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/realm.dart'; import 'package:island/models/realm.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/services/file.dart'; import 'package:island/services/file.dart';
import 'package:island/services/file_uploader.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
@@ -204,19 +204,13 @@ class EditRealmScreen extends HookConsumerWidget {
showLoadingModal(context); showLoadingModal(context);
submitting.value = true; submitting.value = true;
try { try {
final baseUrl = ref.watch(serverUrlProvider);
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Access token is null');
final cloudFile = final cloudFile =
await putFileToCloud( await FileUploader.createCloudFile(
client: ref.read(apiClientProvider),
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,
), ),
atk: token,
baseUrl: baseUrl,
filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg',
).future; ).future;
if (cloudFile == null) { if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...'); throw ArgumentError('Failed to upload the file...');

View File

@@ -7,7 +7,7 @@ part of 'compose_storage_db.dart';
// ************************************************************************** // **************************************************************************
String _$composeStorageNotifierHash() => String _$composeStorageNotifierHash() =>
r'8baf17aa06b6f69641c20645ba8a3dfe01c97f8c'; r'e78dbfd8dbaf728970985aaa2ac4df3575ddfcdf';
/// See also [ComposeStorageNotifier]. /// See also [ComposeStorageNotifier].
@ProviderFor(ComposeStorageNotifier) @ProviderFor(ComposeStorageNotifier)

View File

@@ -2,14 +2,7 @@ import 'dart:async';
import 'dart:ui'; import 'dart:ui';
import 'package:croppy/croppy.dart'; import 'package:croppy/croppy.dart';
import 'package:cross_file/cross_file.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: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( Future<XFile?> cropImage(
BuildContext context, { BuildContext context, {
@@ -17,9 +10,12 @@ Future<XFile?> cropImage(
List<CropAspectRatio?>? allowedAspectRatios, List<CropAspectRatio?>? allowedAspectRatios,
bool replacePath = true, bool replacePath = true,
}) async { }) async {
if (!context.mounted) return null;
final imageBytes = await image.readAsBytes();
if (!context.mounted) return null;
final result = await showMaterialImageCropper( final result = await showMaterialImageCropper(
context, context,
imageProvider: MemoryImage(await image.readAsBytes()), imageProvider: MemoryImage(imageBytes),
showLoadingIndicatorOnSubmit: true, showLoadingIndicatorOnSubmit: true,
allowedAspectRatios: allowedAspectRatios, allowedAspectRatios: allowedAspectRatios,
); );
@@ -38,170 +34,3 @@ Future<XFile?> cropImage(
mimeType: image.mimeType, 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;
}

View File

@@ -1,17 +1,19 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:cross_file/cross_file.dart'; import 'package:cross_file/cross_file.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:mime/mime.dart';
import 'package:native_exif/native_exif.dart';
class FileUploader { class FileUploader {
final Dio _dio; final Dio _client;
FileUploader(this._dio); FileUploader(this._client);
/// Calculates the MD5 hash of a file. /// Calculates the MD5 hash of a file.
Future<String> _calculateFileHash(XFile file) async { Future<String> _calculateFileHash(XFile file) async {
@@ -34,7 +36,7 @@ class FileUploader {
final hash = await _calculateFileHash(file); final hash = await _calculateFileHash(file);
final fileSize = await file.length(); final fileSize = await file.length();
final response = await _dio.post( final response = await _client.post(
'/drive/files/upload/create', '/drive/files/upload/create',
data: { data: {
'hash': hash, 'hash': hash,
@@ -65,7 +67,7 @@ class FileUploader {
), ),
}); });
await _dio.post( await _client.post(
'/drive/files/upload/chunk/$taskId/$chunkIndex', '/drive/files/upload/chunk/$taskId/$chunkIndex',
data: formData, data: formData,
); );
@@ -73,7 +75,7 @@ class FileUploader {
/// Completes the upload and returns the CloudFile object. /// Completes the upload and returns the CloudFile object.
Future<SnCloudFile> completeUpload(String taskId) async { 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); return SnCloudFile.fromJson(response.data);
} }
@@ -146,8 +148,155 @@ class FileUploader {
// Step 3: Complete upload // Step 3: Complete upload
return await completeUpload(taskId); 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 // Riverpod provider for the FileUploader service
final fileUploaderProvider = Provider<FileUploader>((ref) { final fileUploaderProvider = Provider<FileUploader>((ref) {
final dio = ref.watch(apiClientProvider); final dio = ref.watch(apiClientProvider);

View File

@@ -10,6 +10,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/services/file.dart'; import 'package:island/services/file.dart';
import 'package:island/services/file_uploader.dart';
import 'package:island/utils/format.dart'; import 'package:island/utils/format.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
@@ -107,13 +108,23 @@ class AttachmentPreview extends HookConsumerWidget {
static final GlobalKey<SensitiveMarksSelectorState> _sensitiveSelectorKey = static final GlobalKey<SensitiveMarksSelectorState> _sensitiveSelectorKey =
GlobalKey<SensitiveMarksSelectorState>(); GlobalKey<SensitiveMarksSelectorState>();
Future<void> _showRenameDialog(BuildContext context, WidgetRef ref) async { String _getDisplayName() {
final nameController = TextEditingController(text: item.data.name); 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; String? errorMessage;
await showModalBottomSheet( await showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
useRootNavigator: true,
builder: builder:
(context) => SheetScaffold( (context) => SheetScaffold(
heightFactor: 0.6, heightFactor: 0.6,
@@ -152,22 +163,32 @@ class AttachmentPreview extends HookConsumerWidget {
return; return;
} }
try { if (item.isOnCloud) {
showLoadingModal(context); try {
final apiClient = ref.watch(apiClientProvider); showLoadingModal(context);
await apiClient.patch( final apiClient = ref.watch(apiClientProvider);
'/drive/files/${item.data.id}/name', await apiClient.patch(
data: jsonEncode(newName), '/drive/files/${item.data.id}/name',
); data: jsonEncode(newName),
final newData = item.data; );
newData.name = newName; final newData = item.data;
final updatedFile = item.copyWith(data: newData); newData.name = newName;
onUpdate?.call(item.copyWith(data: updatedFile)); 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); if (context.mounted) Navigator.pop(context);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
} }
}, },
child: Text('rename'.tr()), child: Text('rename'.tr()),
@@ -292,6 +313,8 @@ class AttachmentPreview extends HookConsumerWidget {
_ => Symbols.insert_drive_file, _ => Symbols.insert_drive_file,
}; };
final mimeType = FileUploader.getMimeType(item);
if (item.isOnCloud) { if (item.isOnCloud) {
return CloudFileWidget(item: item.data); return CloudFileWidget(item: item.data);
} else if (item.data is XFile) { } else if (item.data is XFile) {
@@ -321,7 +344,12 @@ class AttachmentPreview extends HookConsumerWidget {
children: [ children: [
Icon(fallbackIcon), Icon(fallbackIcon),
const Gap(6), const Gap(6),
Text(file.name, textAlign: TextAlign.center), Text(
_getDisplayName(),
textAlign: TextAlign.center,
),
Text(mimeType, style: TextStyle(fontSize: 10)),
const Gap(1),
FutureBuilder( FutureBuilder(
future: file.length(), future: file.length(),
builder: (context, snapshot) { builder: (context, snapshot) {
@@ -347,6 +375,8 @@ class AttachmentPreview extends HookConsumerWidget {
children: [ children: [
Icon(fallbackIcon), Icon(fallbackIcon),
const Gap(6), const Gap(6),
Text(mimeType, style: TextStyle(fontSize: 10)),
const Gap(1),
Text( Text(
formatFileSize(item.data.length), formatFileSize(item.data.length),
).fontSize(11), ).fontSize(11),
@@ -542,12 +572,20 @@ class AttachmentPreview extends HookConsumerWidget {
onUpdate?.call(item.copyWith(data: result)); 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) if (item.isOnCloud)
MenuAction( MenuAction(
title: 'rename'.tr(), title: 'rename'.tr(),
image: MenuImage.icon(Symbols.edit), image: MenuImage.icon(Symbols.edit),
callback: () async { callback: () async {
await _showRenameDialog(context, ref); await _showRenameSheet(context, ref);
}, },
), ),
if (item.isOnCloud) if (item.isOnCloud)

View File

@@ -6,9 +6,8 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.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/alert.dart';
import 'package:island/widgets/content/attachment_preview.dart'; import 'package:island/widgets/content/attachment_preview.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@@ -42,10 +41,6 @@ class CloudFilePicker extends HookConsumerWidget {
Future<void> startUpload() async { Future<void> startUpload() async {
if (files.value.isEmpty) return; 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); List<SnCloudFile> result = List.empty(growable: true);
uploadProgress.value = 0; uploadProgress.value = 0;
@@ -55,19 +50,9 @@ class CloudFilePicker extends HookConsumerWidget {
uploadPosition.value = idx; uploadPosition.value = idx;
final file = files.value[idx]; final file = files.value[idx];
final cloudFile = final cloudFile =
await putFileToCloud( await FileUploader.createCloudFile(
fileData: file, fileData: file,
atk: token, client: ref.read(apiClientProvider),
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',
},
onProgress: (progress, _) { onProgress: (progress, _) {
uploadProgress.value = progress; uploadProgress.value = progress;
}, },

View File

@@ -32,7 +32,7 @@ class PostComposeCard extends HookConsumerWidget {
final Function(SnPost)? onSubmit; final Function(SnPost)? onSubmit;
final Function(ComposeState)? onStateChanged; final Function(ComposeState)? onStateChanged;
PostComposeCard({ const PostComposeCard({
super.key, super.key,
this.originalPost, this.originalPost,
this.initialState, this.initialState,

View File

@@ -14,9 +14,8 @@ import 'package:island/models/post.dart';
import 'package:island/models/post_category.dart'; import 'package:island/models/post_category.dart';
import 'package:island/models/publisher.dart'; import 'package:island/models/publisher.dart';
import 'package:island/models/realm.dart'; import 'package:island/models/realm.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.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/services/compose_storage_db.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/post/compose_link_attachments.dart'; import 'package:island/widgets/post/compose_link_attachments.dart';
@@ -177,25 +176,14 @@ class ComposeLogic {
try { try {
// Upload any local attachments first // 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++) { for (int i = 0; i < state.attachments.value.length; i++) {
final attachment = state.attachments.value[i]; final attachment = state.attachments.value[i];
if (attachment.data is! SnCloudFile) { if (attachment.data is! SnCloudFile) {
try { try {
final cloudFile = final cloudFile =
await putFileToCloud( await FileUploader.createCloudFile(
client: ref.read(apiClientProvider),
fileData: attachment, 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; ).future;
if (cloudFile != null) { if (cloudFile != null) {
// Update attachments list with cloud file // Update attachments list with cloud file
@@ -509,15 +497,11 @@ class ComposeLogic {
WidgetRef ref, WidgetRef ref,
ComposeState state, ComposeState state,
int index, { int index, {
String? poolId, // For Unit Test String? poolId,
}) async { }) async {
final attachment = state.attachments.value[index]; final attachment = state.attachments.value[index];
if (attachment.isOnCloud) return; 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 { try {
state.attachmentProgress.value = { state.attachmentProgress.value = {
...state.attachmentProgress.value, ...state.attachmentProgress.value,
@@ -530,19 +514,10 @@ class ComposeLogic {
final selectedPoolId = resolveDefaultPoolId(ref, pools); final selectedPoolId = resolveDefaultPoolId(ref, pools);
cloudFile = cloudFile =
await putFileToCloud( await FileUploader.createCloudFile(
client: ref.read(apiClientProvider),
fileData: attachment, fileData: attachment,
atk: token, poolId: poolId ?? selectedPoolId,
baseUrl: baseUrl,
poolId: selectedPoolId,
filename:
attachment.data.name ??
(attachment.type == UniversalFileType.file
? 'General file'
: 'Post media'),
mimetype:
attachment.data.mimeType ??
getMimeTypeFromFileType(attachment.type),
mode: mode:
attachment.type == UniversalFileType.file attachment.type == UniversalFileType.file
? FileUploadMode.generic ? FileUploadMode.generic
@@ -563,7 +538,7 @@ class ComposeLogic {
clone[index] = UniversalFile(data: cloudFile, type: attachment.type); clone[index] = UniversalFile(data: cloudFile, type: attachment.type);
state.attachments.value = clone; state.attachments.value = clone;
} catch (err) { } catch (err) {
showErrorAlert(err.toString()); showErrorAlert(err);
} finally { } finally {
state.attachmentProgress.value = {...state.attachmentProgress.value} state.attachmentProgress.value = {...state.attachmentProgress.value}
..remove(index); ..remove(index);

View File

@@ -1,8 +1,10 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/alert.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.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/models/file.dart';
import 'package:island/pods/link_preview.dart'; import 'package:island/pods/link_preview.dart';
import 'package:island/pods/network.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 'package:mime/mime.dart';
import 'dart:io';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:island/models/chat.dart'; import 'package:island/models/chat.dart';
import 'package:island/screens/chat/chat.dart'; import 'package:island/screens/chat/chat.dart';
@@ -192,7 +190,6 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
final apiClient = ref.read(apiClientProvider); final apiClient = ref.read(apiClientProvider);
final serverUrl = ref.read(serverUrlProvider);
String content = _messageController.text.trim(); String content = _messageController.text.trim();
List<String> attachmentIds = []; List<String> attachmentIds = [];
@@ -216,11 +213,6 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
case ShareContentType.file: case ShareContentType.file:
// Upload files to cloud storage // Upload files to cloud storage
if (widget.content.files?.isNotEmpty == true) { if (widget.content.files?.isNotEmpty == true) {
final token = ref.watch(tokenProvider)?.token;
if (token == null) {
throw Exception('Authentication required');
}
final universalFiles = final universalFiles =
widget.content.files!.map((file) { widget.content.files!.map((file) {
UniversalFileType fileType; UniversalFileType fileType;
@@ -247,19 +239,9 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
for (var idx = 0; idx < universalFiles.length; idx++) { for (var idx = 0; idx < universalFiles.length; idx++) {
final file = universalFiles[idx]; final file = universalFiles[idx];
final cloudFile = final cloudFile =
await putFileToCloud( await FileUploader.createCloudFile(
client: apiClient,
fileData: file, 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, _) { onProgress: (progress, _) {
if (mounted) { if (mounted) {
setState(() { setState(() {