Upload tasks overlay

This commit is contained in:
2025-11-10 01:11:43 +08:00
parent 1395d65b76
commit 8a291c80b7
18 changed files with 582 additions and 196 deletions

View File

@@ -30,6 +30,7 @@ sealed class UploadTask with _$UploadTask {
required UploadTaskStatus status, required UploadTaskStatus status,
required DateTime createdAt, required DateTime createdAt,
required DateTime updatedAt, required DateTime updatedAt,
double? transmissionProgress, // Local file upload progress (0.0-1.0)
String? errorMessage, String? errorMessage,
SnCloudFile? result, SnCloudFile? result,
String? poolId, String? poolId,

View File

@@ -15,7 +15,8 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$UploadTask { mixin _$UploadTask {
String get id; String get taskId; String get fileName; String get contentType; int get fileSize; int get uploadedBytes; int get totalChunks; int get uploadedChunks; UploadTaskStatus get status; DateTime get createdAt; DateTime get updatedAt; String? get errorMessage; SnCloudFile? get result; String? get poolId; String? get bundleId; String? get encryptPassword; String? get expiredAt; String get id; String get taskId; String get fileName; String get contentType; int get fileSize; int get uploadedBytes; int get totalChunks; int get uploadedChunks; UploadTaskStatus get status; DateTime get createdAt; DateTime get updatedAt; double? get transmissionProgress;// Local file upload progress (0.0-1.0)
String? get errorMessage; SnCloudFile? get result; String? get poolId; String? get bundleId; String? get encryptPassword; String? get expiredAt;
/// Create a copy of UploadTask /// Create a copy of UploadTask
/// 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 +29,16 @@ $UploadTaskCopyWith<UploadTask> get copyWith => _$UploadTaskCopyWithImpl<UploadT
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is UploadTask&&(identical(other.id, id) || other.id == id)&&(identical(other.taskId, taskId) || other.taskId == taskId)&&(identical(other.fileName, fileName) || other.fileName == fileName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.fileSize, fileSize) || other.fileSize == fileSize)&&(identical(other.uploadedBytes, uploadedBytes) || other.uploadedBytes == uploadedBytes)&&(identical(other.totalChunks, totalChunks) || other.totalChunks == totalChunks)&&(identical(other.uploadedChunks, uploadedChunks) || other.uploadedChunks == uploadedChunks)&&(identical(other.status, status) || other.status == status)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.result, result) || other.result == result)&&(identical(other.poolId, poolId) || other.poolId == poolId)&&(identical(other.bundleId, bundleId) || other.bundleId == bundleId)&&(identical(other.encryptPassword, encryptPassword) || other.encryptPassword == encryptPassword)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)); return identical(this, other) || (other.runtimeType == runtimeType&&other is UploadTask&&(identical(other.id, id) || other.id == id)&&(identical(other.taskId, taskId) || other.taskId == taskId)&&(identical(other.fileName, fileName) || other.fileName == fileName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.fileSize, fileSize) || other.fileSize == fileSize)&&(identical(other.uploadedBytes, uploadedBytes) || other.uploadedBytes == uploadedBytes)&&(identical(other.totalChunks, totalChunks) || other.totalChunks == totalChunks)&&(identical(other.uploadedChunks, uploadedChunks) || other.uploadedChunks == uploadedChunks)&&(identical(other.status, status) || other.status == status)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.transmissionProgress, transmissionProgress) || other.transmissionProgress == transmissionProgress)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.result, result) || other.result == result)&&(identical(other.poolId, poolId) || other.poolId == poolId)&&(identical(other.bundleId, bundleId) || other.bundleId == bundleId)&&(identical(other.encryptPassword, encryptPassword) || other.encryptPassword == encryptPassword)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,taskId,fileName,contentType,fileSize,uploadedBytes,totalChunks,uploadedChunks,status,createdAt,updatedAt,errorMessage,result,poolId,bundleId,encryptPassword,expiredAt); int get hashCode => Object.hash(runtimeType,id,taskId,fileName,contentType,fileSize,uploadedBytes,totalChunks,uploadedChunks,status,createdAt,updatedAt,transmissionProgress,errorMessage,result,poolId,bundleId,encryptPassword,expiredAt);
@override @override
String toString() { String toString() {
return 'UploadTask(id: $id, taskId: $taskId, fileName: $fileName, contentType: $contentType, fileSize: $fileSize, uploadedBytes: $uploadedBytes, totalChunks: $totalChunks, uploadedChunks: $uploadedChunks, status: $status, createdAt: $createdAt, updatedAt: $updatedAt, errorMessage: $errorMessage, result: $result, poolId: $poolId, bundleId: $bundleId, encryptPassword: $encryptPassword, expiredAt: $expiredAt)'; return 'UploadTask(id: $id, taskId: $taskId, fileName: $fileName, contentType: $contentType, fileSize: $fileSize, uploadedBytes: $uploadedBytes, totalChunks: $totalChunks, uploadedChunks: $uploadedChunks, status: $status, createdAt: $createdAt, updatedAt: $updatedAt, transmissionProgress: $transmissionProgress, errorMessage: $errorMessage, result: $result, poolId: $poolId, bundleId: $bundleId, encryptPassword: $encryptPassword, expiredAt: $expiredAt)';
} }
@@ -48,7 +49,7 @@ abstract mixin class $UploadTaskCopyWith<$Res> {
factory $UploadTaskCopyWith(UploadTask value, $Res Function(UploadTask) _then) = _$UploadTaskCopyWithImpl; factory $UploadTaskCopyWith(UploadTask value, $Res Function(UploadTask) _then) = _$UploadTaskCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, UploadTaskStatus status, DateTime createdAt, DateTime updatedAt, String? errorMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, UploadTaskStatus status, DateTime createdAt, DateTime updatedAt, double? transmissionProgress, String? errorMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt
}); });
@@ -65,7 +66,7 @@ class _$UploadTaskCopyWithImpl<$Res>
/// Create a copy of UploadTask /// Create a copy of UploadTask
/// 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? id = null,Object? taskId = null,Object? fileName = null,Object? contentType = null,Object? fileSize = null,Object? uploadedBytes = null,Object? totalChunks = null,Object? uploadedChunks = null,Object? status = null,Object? createdAt = null,Object? updatedAt = null,Object? errorMessage = freezed,Object? result = freezed,Object? poolId = freezed,Object? bundleId = freezed,Object? encryptPassword = freezed,Object? expiredAt = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? taskId = null,Object? fileName = null,Object? contentType = null,Object? fileSize = null,Object? uploadedBytes = null,Object? totalChunks = null,Object? uploadedChunks = null,Object? status = null,Object? createdAt = null,Object? updatedAt = null,Object? transmissionProgress = freezed,Object? errorMessage = freezed,Object? result = freezed,Object? poolId = freezed,Object? bundleId = freezed,Object? encryptPassword = freezed,Object? expiredAt = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,taskId: null == taskId ? _self.taskId : taskId // ignore: cast_nullable_to_non_nullable as String,taskId: null == taskId ? _self.taskId : taskId // ignore: cast_nullable_to_non_nullable
@@ -78,7 +79,8 @@ as int,uploadedChunks: null == uploadedChunks ? _self.uploadedChunks : uploadedC
as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as UploadTaskStatus,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as UploadTaskStatus,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable as DateTime,transmissionProgress: freezed == transmissionProgress ? _self.transmissionProgress : transmissionProgress // ignore: cast_nullable_to_non_nullable
as double?,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String?,result: freezed == result ? _self.result : result // ignore: cast_nullable_to_non_nullable as String?,result: freezed == result ? _self.result : result // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,poolId: freezed == poolId ? _self.poolId : poolId // ignore: cast_nullable_to_non_nullable as SnCloudFile?,poolId: freezed == poolId ? _self.poolId : poolId // ignore: cast_nullable_to_non_nullable
as String?,bundleId: freezed == bundleId ? _self.bundleId : bundleId // ignore: cast_nullable_to_non_nullable as String?,bundleId: freezed == bundleId ? _self.bundleId : bundleId // ignore: cast_nullable_to_non_nullable
@@ -178,10 +180,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, UploadTaskStatus status, DateTime createdAt, DateTime updatedAt, String? errorMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, UploadTaskStatus status, DateTime createdAt, DateTime updatedAt, double? transmissionProgress, String? errorMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _UploadTask() when $default != null: case _UploadTask() when $default != null:
return $default(_that.id,_that.taskId,_that.fileName,_that.contentType,_that.fileSize,_that.uploadedBytes,_that.totalChunks,_that.uploadedChunks,_that.status,_that.createdAt,_that.updatedAt,_that.errorMessage,_that.result,_that.poolId,_that.bundleId,_that.encryptPassword,_that.expiredAt);case _: return $default(_that.id,_that.taskId,_that.fileName,_that.contentType,_that.fileSize,_that.uploadedBytes,_that.totalChunks,_that.uploadedChunks,_that.status,_that.createdAt,_that.updatedAt,_that.transmissionProgress,_that.errorMessage,_that.result,_that.poolId,_that.bundleId,_that.encryptPassword,_that.expiredAt);case _:
return orElse(); return orElse();
} }
@@ -199,10 +201,10 @@ return $default(_that.id,_that.taskId,_that.fileName,_that.contentType,_that.fil
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, UploadTaskStatus status, DateTime createdAt, DateTime updatedAt, String? errorMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, UploadTaskStatus status, DateTime createdAt, DateTime updatedAt, double? transmissionProgress, String? errorMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _UploadTask(): case _UploadTask():
return $default(_that.id,_that.taskId,_that.fileName,_that.contentType,_that.fileSize,_that.uploadedBytes,_that.totalChunks,_that.uploadedChunks,_that.status,_that.createdAt,_that.updatedAt,_that.errorMessage,_that.result,_that.poolId,_that.bundleId,_that.encryptPassword,_that.expiredAt);} return $default(_that.id,_that.taskId,_that.fileName,_that.contentType,_that.fileSize,_that.uploadedBytes,_that.totalChunks,_that.uploadedChunks,_that.status,_that.createdAt,_that.updatedAt,_that.transmissionProgress,_that.errorMessage,_that.result,_that.poolId,_that.bundleId,_that.encryptPassword,_that.expiredAt);}
} }
/// A variant of `when` that fallback to returning `null` /// A variant of `when` that fallback to returning `null`
/// ///
@@ -216,10 +218,10 @@ return $default(_that.id,_that.taskId,_that.fileName,_that.contentType,_that.fil
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, UploadTaskStatus status, DateTime createdAt, DateTime updatedAt, String? errorMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, UploadTaskStatus status, DateTime createdAt, DateTime updatedAt, double? transmissionProgress, String? errorMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _UploadTask() when $default != null: case _UploadTask() when $default != null:
return $default(_that.id,_that.taskId,_that.fileName,_that.contentType,_that.fileSize,_that.uploadedBytes,_that.totalChunks,_that.uploadedChunks,_that.status,_that.createdAt,_that.updatedAt,_that.errorMessage,_that.result,_that.poolId,_that.bundleId,_that.encryptPassword,_that.expiredAt);case _: return $default(_that.id,_that.taskId,_that.fileName,_that.contentType,_that.fileSize,_that.uploadedBytes,_that.totalChunks,_that.uploadedChunks,_that.status,_that.createdAt,_that.updatedAt,_that.transmissionProgress,_that.errorMessage,_that.result,_that.poolId,_that.bundleId,_that.encryptPassword,_that.expiredAt);case _:
return null; return null;
} }
@@ -231,7 +233,7 @@ return $default(_that.id,_that.taskId,_that.fileName,_that.contentType,_that.fil
@JsonSerializable() @JsonSerializable()
class _UploadTask extends UploadTask { class _UploadTask extends UploadTask {
const _UploadTask({required this.id, required this.taskId, required this.fileName, required this.contentType, required this.fileSize, required this.uploadedBytes, required this.totalChunks, required this.uploadedChunks, required this.status, required this.createdAt, required this.updatedAt, this.errorMessage, this.result, this.poolId, this.bundleId, this.encryptPassword, this.expiredAt}): super._(); const _UploadTask({required this.id, required this.taskId, required this.fileName, required this.contentType, required this.fileSize, required this.uploadedBytes, required this.totalChunks, required this.uploadedChunks, required this.status, required this.createdAt, required this.updatedAt, this.transmissionProgress, this.errorMessage, this.result, this.poolId, this.bundleId, this.encryptPassword, this.expiredAt}): super._();
factory _UploadTask.fromJson(Map<String, dynamic> json) => _$UploadTaskFromJson(json); factory _UploadTask.fromJson(Map<String, dynamic> json) => _$UploadTaskFromJson(json);
@override final String id; @override final String id;
@@ -245,6 +247,8 @@ class _UploadTask extends UploadTask {
@override final UploadTaskStatus status; @override final UploadTaskStatus status;
@override final DateTime createdAt; @override final DateTime createdAt;
@override final DateTime updatedAt; @override final DateTime updatedAt;
@override final double? transmissionProgress;
// Local file upload progress (0.0-1.0)
@override final String? errorMessage; @override final String? errorMessage;
@override final SnCloudFile? result; @override final SnCloudFile? result;
@override final String? poolId; @override final String? poolId;
@@ -265,16 +269,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _UploadTask&&(identical(other.id, id) || other.id == id)&&(identical(other.taskId, taskId) || other.taskId == taskId)&&(identical(other.fileName, fileName) || other.fileName == fileName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.fileSize, fileSize) || other.fileSize == fileSize)&&(identical(other.uploadedBytes, uploadedBytes) || other.uploadedBytes == uploadedBytes)&&(identical(other.totalChunks, totalChunks) || other.totalChunks == totalChunks)&&(identical(other.uploadedChunks, uploadedChunks) || other.uploadedChunks == uploadedChunks)&&(identical(other.status, status) || other.status == status)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.result, result) || other.result == result)&&(identical(other.poolId, poolId) || other.poolId == poolId)&&(identical(other.bundleId, bundleId) || other.bundleId == bundleId)&&(identical(other.encryptPassword, encryptPassword) || other.encryptPassword == encryptPassword)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _UploadTask&&(identical(other.id, id) || other.id == id)&&(identical(other.taskId, taskId) || other.taskId == taskId)&&(identical(other.fileName, fileName) || other.fileName == fileName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.fileSize, fileSize) || other.fileSize == fileSize)&&(identical(other.uploadedBytes, uploadedBytes) || other.uploadedBytes == uploadedBytes)&&(identical(other.totalChunks, totalChunks) || other.totalChunks == totalChunks)&&(identical(other.uploadedChunks, uploadedChunks) || other.uploadedChunks == uploadedChunks)&&(identical(other.status, status) || other.status == status)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.transmissionProgress, transmissionProgress) || other.transmissionProgress == transmissionProgress)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.result, result) || other.result == result)&&(identical(other.poolId, poolId) || other.poolId == poolId)&&(identical(other.bundleId, bundleId) || other.bundleId == bundleId)&&(identical(other.encryptPassword, encryptPassword) || other.encryptPassword == encryptPassword)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,taskId,fileName,contentType,fileSize,uploadedBytes,totalChunks,uploadedChunks,status,createdAt,updatedAt,errorMessage,result,poolId,bundleId,encryptPassword,expiredAt); int get hashCode => Object.hash(runtimeType,id,taskId,fileName,contentType,fileSize,uploadedBytes,totalChunks,uploadedChunks,status,createdAt,updatedAt,transmissionProgress,errorMessage,result,poolId,bundleId,encryptPassword,expiredAt);
@override @override
String toString() { String toString() {
return 'UploadTask(id: $id, taskId: $taskId, fileName: $fileName, contentType: $contentType, fileSize: $fileSize, uploadedBytes: $uploadedBytes, totalChunks: $totalChunks, uploadedChunks: $uploadedChunks, status: $status, createdAt: $createdAt, updatedAt: $updatedAt, errorMessage: $errorMessage, result: $result, poolId: $poolId, bundleId: $bundleId, encryptPassword: $encryptPassword, expiredAt: $expiredAt)'; return 'UploadTask(id: $id, taskId: $taskId, fileName: $fileName, contentType: $contentType, fileSize: $fileSize, uploadedBytes: $uploadedBytes, totalChunks: $totalChunks, uploadedChunks: $uploadedChunks, status: $status, createdAt: $createdAt, updatedAt: $updatedAt, transmissionProgress: $transmissionProgress, errorMessage: $errorMessage, result: $result, poolId: $poolId, bundleId: $bundleId, encryptPassword: $encryptPassword, expiredAt: $expiredAt)';
} }
@@ -285,7 +289,7 @@ abstract mixin class _$UploadTaskCopyWith<$Res> implements $UploadTaskCopyWith<$
factory _$UploadTaskCopyWith(_UploadTask value, $Res Function(_UploadTask) _then) = __$UploadTaskCopyWithImpl; factory _$UploadTaskCopyWith(_UploadTask value, $Res Function(_UploadTask) _then) = __$UploadTaskCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, UploadTaskStatus status, DateTime createdAt, DateTime updatedAt, String? errorMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, UploadTaskStatus status, DateTime createdAt, DateTime updatedAt, double? transmissionProgress, String? errorMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt
}); });
@@ -302,7 +306,7 @@ class __$UploadTaskCopyWithImpl<$Res>
/// Create a copy of UploadTask /// Create a copy of UploadTask
/// 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? id = null,Object? taskId = null,Object? fileName = null,Object? contentType = null,Object? fileSize = null,Object? uploadedBytes = null,Object? totalChunks = null,Object? uploadedChunks = null,Object? status = null,Object? createdAt = null,Object? updatedAt = null,Object? errorMessage = freezed,Object? result = freezed,Object? poolId = freezed,Object? bundleId = freezed,Object? encryptPassword = freezed,Object? expiredAt = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? taskId = null,Object? fileName = null,Object? contentType = null,Object? fileSize = null,Object? uploadedBytes = null,Object? totalChunks = null,Object? uploadedChunks = null,Object? status = null,Object? createdAt = null,Object? updatedAt = null,Object? transmissionProgress = freezed,Object? errorMessage = freezed,Object? result = freezed,Object? poolId = freezed,Object? bundleId = freezed,Object? encryptPassword = freezed,Object? expiredAt = freezed,}) {
return _then(_UploadTask( return _then(_UploadTask(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,taskId: null == taskId ? _self.taskId : taskId // ignore: cast_nullable_to_non_nullable as String,taskId: null == taskId ? _self.taskId : taskId // ignore: cast_nullable_to_non_nullable
@@ -315,7 +319,8 @@ as int,uploadedChunks: null == uploadedChunks ? _self.uploadedChunks : uploadedC
as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as UploadTaskStatus,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as UploadTaskStatus,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable as DateTime,transmissionProgress: freezed == transmissionProgress ? _self.transmissionProgress : transmissionProgress // ignore: cast_nullable_to_non_nullable
as double?,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String?,result: freezed == result ? _self.result : result // ignore: cast_nullable_to_non_nullable as String?,result: freezed == result ? _self.result : result // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,poolId: freezed == poolId ? _self.poolId : poolId // ignore: cast_nullable_to_non_nullable as SnCloudFile?,poolId: freezed == poolId ? _self.poolId : poolId // ignore: cast_nullable_to_non_nullable
as String?,bundleId: freezed == bundleId ? _self.bundleId : bundleId // ignore: cast_nullable_to_non_nullable as String?,bundleId: freezed == bundleId ? _self.bundleId : bundleId // ignore: cast_nullable_to_non_nullable

View File

@@ -18,6 +18,7 @@ _UploadTask _$UploadTaskFromJson(Map<String, dynamic> json) => _UploadTask(
status: $enumDecode(_$UploadTaskStatusEnumMap, json['status']), status: $enumDecode(_$UploadTaskStatusEnumMap, json['status']),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
transmissionProgress: (json['transmission_progress'] as num?)?.toDouble(),
errorMessage: json['error_message'] as String?, errorMessage: json['error_message'] as String?,
result: result:
json['result'] == null json['result'] == null
@@ -42,6 +43,7 @@ Map<String, dynamic> _$UploadTaskToJson(_UploadTask instance) =>
'status': _$UploadTaskStatusEnumMap[instance.status]!, 'status': _$UploadTaskStatusEnumMap[instance.status]!,
'created_at': instance.createdAt.toIso8601String(), 'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(),
'transmission_progress': instance.transmissionProgress,
'error_message': instance.errorMessage, 'error_message': instance.errorMessage,
'result': instance.result?.toJson(), 'result': instance.result?.toJson(),
'pool_id': instance.poolId, 'pool_id': instance.poolId,

View File

@@ -3,6 +3,7 @@ import "package:dio/dio.dart";
import "package:drift/drift.dart" show Variable; import "package:drift/drift.dart" show Variable;
import "package:easy_localization/easy_localization.dart"; import "package:easy_localization/easy_localization.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:island/database/drift_db.dart"; 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";
@@ -433,6 +434,7 @@ class MessagesNotifier extends _$MessagesNotifier {
} }
Future<void> sendMessage( Future<void> sendMessage(
WidgetRef ref,
String content, String content,
List<UniversalFile> attachments, { List<UniversalFile> attachments, {
SnChatMessage? editingTo, SnChatMessage? editingTo,
@@ -471,8 +473,8 @@ class MessagesNotifier extends _$MessagesNotifier {
for (var idx = 0; idx < attachments.length; idx++) { for (var idx = 0; idx < attachments.length; idx++) {
final cloudFile = final cloudFile =
await FileUploader.createCloudFile( await FileUploader.createCloudFile(
ref: ref,
fileData: attachments[idx], fileData: attachments[idx],
client: ref.read(apiClientProvider),
onProgress: (progress, _) { onProgress: (progress, _) {
_fileUploadProgress[localMessage.id]?[idx] = progress ?? 0.0; _fileUploadProgress[localMessage.id]?[idx] = progress ?? 0.0;
onProgress?.call( onProgress?.call(

View File

@@ -7,6 +7,7 @@ import 'package:island/models/upload_task.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:island/services/file_uploader.dart'; import 'package:island/services/file_uploader.dart';
import 'package:island/talker.dart';
final uploadTasksProvider = final uploadTasksProvider =
StateNotifierProvider<UploadTasksNotifier, List<UploadTask>>( StateNotifierProvider<UploadTasksNotifier, List<UploadTask>>(
@@ -16,6 +17,7 @@ final uploadTasksProvider =
class UploadTasksNotifier extends StateNotifier<List<UploadTask>> { class UploadTasksNotifier extends StateNotifier<List<UploadTask>> {
final Ref ref; final Ref ref;
StreamSubscription? _websocketSubscription; StreamSubscription? _websocketSubscription;
final Map<String, Map<String, dynamic>> _pendingUploads = {};
UploadTasksNotifier(this.ref) : super([]) { UploadTasksNotifier(this.ref) : super([]) {
_listenToWebSocket(); _listenToWebSocket();
@@ -29,40 +31,121 @@ class UploadTasksNotifier extends StateNotifier<List<UploadTask>> {
} }
void _handleWebSocketPacket(dynamic packet) { void _handleWebSocketPacket(dynamic packet) {
if (packet.type.startsWith('upload.')) { if (packet.type.startsWith('task.')) {
final data = packet.data; final data = packet.data;
if (data == null) return; if (data == null) return;
// Debug logging
talker.info(
'[UploadTasks] Received WebSocket packet: ${packet.type}, data: $data',
);
final taskId = data['task_id'] as String?; final taskId = data['task_id'] as String?;
if (taskId == null) return; if (taskId == null) return;
switch (packet.type) { switch (packet.type) {
case 'upload.progress': case 'task.created':
_handleTaskCreated(taskId, data);
break;
case 'task.progress':
_handleProgressUpdate(taskId, data); _handleProgressUpdate(taskId, data);
break; break;
case 'upload.completed': case 'task.completed':
_handleUploadCompleted(taskId, data); _handleUploadCompleted(taskId, data);
break; break;
case 'upload.failed': case 'task.failed':
_handleUploadFailed(taskId, data); _handleUploadFailed(taskId, data);
break; break;
} }
} }
} }
void _handleProgressUpdate(String taskId, Map<String, dynamic> data) { void _handleTaskCreated(String taskId, Map<String, dynamic> data) {
final uploadedChunks = data['chunksUploaded'] as int? ?? 0; talker.info('[UploadTasks] Handling task.created for taskId: $taskId');
final uploadedBytes =
(data['progress'] as num? ?? 0.0) /
100.0 *
(data['fileSize'] as int? ?? 0);
// Check if task already exists (might have been created locally)
final existingTask =
state.where((task) => task.taskId == taskId).firstOrNull;
if (existingTask != null) {
talker.info('[UploadTasks] Task already exists, updating status');
// Task already exists, just update its status to confirm server creation
state = state =
state.map((task) { state.map((task) {
if (task.taskId == taskId) { if (task.taskId == taskId) {
return task.copyWith( return task.copyWith(
uploadedChunks: uploadedChunks, status: UploadTaskStatus.pending,
uploadedBytes: uploadedBytes.toInt(), updatedAt: DateTime.now(),
);
}
return task;
}).toList();
return;
}
// Check if we have stored metadata for this task
final metadata = _pendingUploads[taskId];
talker.info('[UploadTasks] Metadata for taskId $taskId: $metadata');
if (metadata != null) {
talker.info('[UploadTasks] Creating task with full metadata');
// Create task with full metadata
final uploadTask = UploadTask(
id: DateTime.now().millisecondsSinceEpoch.toString(),
taskId: taskId,
fileName: metadata['fileName'] as String,
contentType: metadata['contentType'] as String,
fileSize: metadata['fileSize'] as int,
uploadedBytes: 0,
totalChunks: metadata['totalChunks'] as int,
uploadedChunks: 0,
status: UploadTaskStatus.pending,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
poolId: metadata['poolId'] as String?,
bundleId: metadata['bundleId'] as String?,
encryptPassword: metadata['encryptPassword'] as String?,
expiredAt: metadata['expiredAt'] as String?,
);
state = [...state, uploadTask];
talker.info(
'[UploadTasks] Task created successfully. Total tasks: ${state.length}',
);
// Clean up stored metadata
_pendingUploads.remove(taskId);
} else {
talker.info('[UploadTasks] No metadata found, creating minimal task');
// Create minimal task if no metadata is stored
final uploadTask = UploadTask(
id: DateTime.now().millisecondsSinceEpoch.toString(),
taskId: taskId,
fileName: data['name'] as String? ?? 'Unknown file',
contentType: 'application/octet-stream',
fileSize: 0,
uploadedBytes: 0,
totalChunks: 0,
uploadedChunks: 0,
status: UploadTaskStatus.pending,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
state = [...state, uploadTask];
talker.info(
'[UploadTasks] Minimal task created. Total tasks: ${state.length}',
);
}
}
void _handleProgressUpdate(String taskId, Map<String, dynamic> data) {
final progress = data['progress'] as num? ?? 0.0;
state =
state.map((task) {
if (task.taskId == taskId) {
final uploadedBytes = (progress / 100.0 * task.fileSize).toInt();
return task.copyWith(
uploadedBytes: uploadedBytes,
status: UploadTaskStatus.inProgress, status: UploadTaskStatus.inProgress,
updatedAt: DateTime.now(), updatedAt: DateTime.now(),
); );
@@ -72,11 +155,7 @@ class UploadTasksNotifier extends StateNotifier<List<UploadTask>> {
} }
void _handleUploadCompleted(String taskId, Map<String, dynamic> data) { void _handleUploadCompleted(String taskId, Map<String, dynamic> data) {
final fileData = data['file']; final results = data['results'] as Map<String, dynamic>?;
if (fileData != null) {
// Assuming the file data comes in the expected format
// You might need to adjust this based on the actual API response
}
state = state =
state.map((task) { state.map((task) {
@@ -85,6 +164,14 @@ class UploadTasksNotifier extends StateNotifier<List<UploadTask>> {
status: UploadTaskStatus.completed, status: UploadTaskStatus.completed,
uploadedChunks: task.totalChunks, uploadedChunks: task.totalChunks,
uploadedBytes: task.fileSize, uploadedBytes: task.fileSize,
// Update file information from Results if available
fileName: results?['file_name'] as String? ?? task.fileName,
fileSize: results?['file_size'] as int? ?? task.fileSize,
contentType: results?['mime_type'] as String? ?? task.contentType,
result:
results?['file_info'] != null
? SnCloudFile.fromJson(results!['file_info'])
: null,
updatedAt: DateTime.now(), updatedAt: DateTime.now(),
); );
} }
@@ -93,7 +180,7 @@ class UploadTasksNotifier extends StateNotifier<List<UploadTask>> {
} }
void _handleUploadFailed(String taskId, Map<String, dynamic> data) { void _handleUploadFailed(String taskId, Map<String, dynamic> data) {
final errorMessage = data['error'] as String? ?? 'Upload failed'; final errorMessage = data['error_message'] as String? ?? 'Upload failed';
state = state =
state.map((task) { state.map((task) {
@@ -112,6 +199,29 @@ class UploadTasksNotifier extends StateNotifier<List<UploadTask>> {
state = [...state, task]; state = [...state, task];
} }
void storeUploadMetadata(
String taskId, {
required String fileName,
required String contentType,
required int fileSize,
required int totalChunks,
String? poolId,
String? bundleId,
String? encryptPassword,
String? expiredAt,
}) {
_pendingUploads[taskId] = {
'fileName': fileName,
'contentType': contentType,
'fileSize': fileSize,
'totalChunks': totalChunks,
'poolId': poolId,
'bundleId': bundleId,
'encryptPassword': encryptPassword,
'expiredAt': expiredAt,
};
}
void updateTaskStatus( void updateTaskStatus(
String taskId, String taskId,
UploadTaskStatus status, { UploadTaskStatus status, {
@@ -130,10 +240,36 @@ class UploadTasksNotifier extends StateNotifier<List<UploadTask>> {
}).toList(); }).toList();
} }
void updateTransmissionProgress(String taskId, double progress) {
state =
state.map((task) {
if (task.taskId == taskId) {
return task.copyWith(
transmissionProgress: progress,
updatedAt: DateTime.now(),
);
}
return task;
}).toList();
}
void removeTask(String taskId) { void removeTask(String taskId) {
state = state.where((task) => task.taskId != taskId).toList(); state = state.where((task) => task.taskId != taskId).toList();
} }
void clearCompletedTasks() {
state =
state
.where(
(task) =>
task.status != UploadTaskStatus.completed &&
task.status != UploadTaskStatus.failed &&
task.status != UploadTaskStatus.cancelled &&
task.status != UploadTaskStatus.expired,
)
.toList();
}
UploadTask? getTask(String taskId) { UploadTask? getTask(String taskId) {
return state.where((task) => task.taskId == taskId).firstOrNull; return state.where((task) => task.taskId == taskId).firstOrNull;
} }
@@ -144,7 +280,8 @@ class UploadTasksNotifier extends StateNotifier<List<UploadTask>> {
(task) => (task) =>
task.status == UploadTaskStatus.pending || task.status == UploadTaskStatus.pending ||
task.status == UploadTaskStatus.inProgress || task.status == UploadTaskStatus.inProgress ||
task.status == UploadTaskStatus.paused, task.status == UploadTaskStatus.paused ||
task.status == UploadTaskStatus.completed,
) )
.toList(); .toList();
} }
@@ -222,14 +359,6 @@ class EnhancedFileUploader extends FileUploader {
chunkSize: customChunkSize, chunkSize: customChunkSize,
); );
if (createResponse['file_exists'] == true) {
// File already exists, return the existing file
return SnCloudFile.fromJson(createResponse['file']);
}
final taskId = createResponse['task_id'] as String;
final chunkSize = createResponse['chunk_size'] as int;
final chunksCount = createResponse['chunks_count'] as int;
int totalSize; int totalSize;
if (fileData is XFile) { if (fileData is XFile) {
totalSize = await fileData.length(); totalSize = await fileData.length();
@@ -239,17 +368,26 @@ class EnhancedFileUploader extends FileUploader {
throw ArgumentError('Invalid fileData type'); throw ArgumentError('Invalid fileData type');
} }
// Create upload task and add to state if (createResponse['file_exists'] == true) {
// File already exists, create a local task to show it was found
final existingFile = SnCloudFile.fromJson(createResponse['file']);
// Create a task that shows as completed immediately
// Use a generated taskId since the server might not provide one for existing files
final taskId =
createResponse['task_id'] as String? ??
'existing-${DateTime.now().millisecondsSinceEpoch}';
final uploadTask = UploadTask( final uploadTask = UploadTask(
id: DateTime.now().millisecondsSinceEpoch.toString(), id: DateTime.now().millisecondsSinceEpoch.toString(),
taskId: taskId, taskId: taskId,
fileName: fileName, fileName: fileName,
contentType: contentType, contentType: contentType,
fileSize: totalSize, fileSize: totalSize,
uploadedBytes: 0, uploadedBytes: totalSize,
totalChunks: chunksCount, totalChunks: 1, // For existing files, we consider it as 1 chunk
uploadedChunks: 0, uploadedChunks: 1,
status: UploadTaskStatus.pending, status: UploadTaskStatus.completed,
createdAt: DateTime.now(), createdAt: DateTime.now(),
updatedAt: DateTime.now(), updatedAt: DateTime.now(),
poolId: poolId, poolId: poolId,
@@ -260,6 +398,29 @@ class EnhancedFileUploader extends FileUploader {
ref.read(uploadTasksProvider.notifier).addUploadTask(uploadTask); ref.read(uploadTasksProvider.notifier).addUploadTask(uploadTask);
return existingFile;
}
final taskId = createResponse['task_id'] as String;
final chunkSize = createResponse['chunk_size'] as int;
final chunksCount = createResponse['chunks_count'] as int;
// Store upload metadata for when task.created event arrives
talker.info('[UploadTasks] Storing metadata for taskId: $taskId');
ref
.read(uploadTasksProvider.notifier)
.storeUploadMetadata(
taskId,
fileName: fileName,
contentType: contentType,
fileSize: totalSize,
totalChunks: chunksCount,
poolId: poolId,
bundleId: bundleId,
encryptPassword: encryptPassword,
expiredAt: expiredAt,
);
// Step 2: Upload chunks // Step 2: Upload chunks
int bytesUploaded = 0; int bytesUploaded = 0;
if (fileData is XFile) { if (fileData is XFile) {
@@ -279,6 +440,10 @@ class EnhancedFileUploader extends FileUploader {
onSendProgress: (sent, total) { onSendProgress: (sent, total) {
final overallProgress = (bytesUploaded + sent) / totalSize; final overallProgress = (bytesUploaded + sent) / totalSize;
onProgress?.call(overallProgress, Duration.zero); onProgress?.call(overallProgress, Duration.zero);
// Update transmission progress in UI
ref
.read(uploadTasksProvider.notifier)
.updateTransmissionProgress(taskId, overallProgress);
}, },
); );
bytesUploaded += chunkData.length; bytesUploaded += chunkData.length;
@@ -302,6 +467,10 @@ class EnhancedFileUploader extends FileUploader {
onSendProgress: (sent, total) { onSendProgress: (sent, total) {
final overallProgress = (bytesUploaded + sent) / totalSize; final overallProgress = (bytesUploaded + sent) / totalSize;
onProgress?.call(overallProgress, Duration.zero); onProgress?.call(overallProgress, Duration.zero);
// Update transmission progress in UI
ref
.read(uploadTasksProvider.notifier)
.updateTransmissionProgress(taskId, overallProgress);
}, },
); );
bytesUploaded += chunks[i].length; bytesUploaded += chunks[i].length;

View File

@@ -76,7 +76,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
try { try {
final cloudFile = final cloudFile =
await FileUploader.createCloudFile( await FileUploader.createCloudFile(
client: ref.read(apiClientProvider), ref: ref,
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,

View File

@@ -99,7 +99,7 @@ class EditChatScreen extends HookConsumerWidget {
try { try {
final cloudFile = final cloudFile =
await FileUploader.createCloudFile( await FileUploader.createCloudFile(
client: ref.read(apiClientProvider), ref: ref,
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,

View File

@@ -265,6 +265,7 @@ class ChatRoomScreen extends HookConsumerWidget {
if (messageController.text.trim().isNotEmpty || if (messageController.text.trim().isNotEmpty ||
attachments.value.isNotEmpty) { attachments.value.isNotEmpty) {
messagesNotifier.sendMessage( messagesNotifier.sendMessage(
ref,
messageController.text.trim(), messageController.text.trim(),
attachments.value, attachments.value,
editingTo: messageEditingTo.value, editingTo: messageEditingTo.value,
@@ -561,7 +562,7 @@ class ChatRoomScreen extends HookConsumerWidget {
final cloudFile = final cloudFile =
await FileUploader.createCloudFile( await FileUploader.createCloudFile(
client: ref.read(apiClientProvider), ref: ref,
fileData: attachment, fileData: attachment,
poolId: config.poolId, poolId: config.poolId,
mode: mode:

View File

@@ -95,11 +95,11 @@ class EditPublisherScreen extends HookConsumerWidget {
try { try {
final cloudFile = final cloudFile =
await FileUploader.createCloudFile( await FileUploader.createCloudFile(
ref: ref,
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,
), ),
client: ref.read(apiClientProvider),
).future; ).future;
if (cloudFile == null) { if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...'); throw ArgumentError('Failed to upload the file...');

View File

@@ -141,7 +141,7 @@ class EditAppScreen extends HookConsumerWidget {
try { try {
final cloudFile = final cloudFile =
await FileUploader.createCloudFile( await FileUploader.createCloudFile(
client: ref.read(apiClientProvider), ref: ref,
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,

View File

@@ -127,11 +127,11 @@ class EditBotScreen extends HookConsumerWidget {
try { try {
final cloudFile = final cloudFile =
await FileUploader.createCloudFile( await FileUploader.createCloudFile(
ref: ref,
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,
), ),
client: ref.read(apiClientProvider),
).future; ).future;
if (cloudFile == null) { if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...'); throw ArgumentError('Failed to upload the file...');

View File

@@ -92,7 +92,7 @@ class EditRealmScreen extends HookConsumerWidget {
try { try {
final cloudFile = final cloudFile =
await FileUploader.createCloudFile( await FileUploader.createCloudFile(
client: ref.read(apiClientProvider), ref: ref,
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,

View File

@@ -7,6 +7,7 @@ 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:island/pods/upload_tasks.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:native_exif/native_exif.dart'; import 'package:native_exif/native_exif.dart';
import 'package:path/path.dart' show extension; import 'package:path/path.dart' show extension;
@@ -235,7 +236,7 @@ class FileUploader {
static Completer<SnCloudFile?> createCloudFile({ static Completer<SnCloudFile?> createCloudFile({
required UniversalFile fileData, required UniversalFile fileData,
required Dio client, required WidgetRef ref,
String? poolId, String? poolId,
FileUploadMode? mode, FileUploadMode? mode,
Function(double? progress, Duration estimate)? onProgress, Function(double? progress, Duration estimate)? onProgress,
@@ -272,19 +273,14 @@ class FileUploader {
await exif.writeAttributes(gpsAttributes); await exif.writeAttributes(gpsAttributes);
}) })
.then( .then(
(_) => _processUploadWithEnhancedUploader( (_) =>
fileData, _processUpload(fileData, ref, poolId, onProgress, completer),
client,
poolId,
onProgress,
completer,
),
) )
.catchError((e) { .catchError((e) {
debugPrint('Error removing GPS EXIF data: $e'); debugPrint('Error removing GPS EXIF data: $e');
return _processUploadWithEnhancedUploader( return _processUpload(
fileData, fileData,
client, ref,
poolId, poolId,
onProgress, onProgress,
completer, completer,
@@ -295,20 +291,14 @@ class FileUploader {
} }
} }
_processUploadWithEnhancedUploader( _processUpload(fileData, ref, poolId, onProgress, completer);
fileData,
client,
poolId,
onProgress,
completer,
);
return completer; return completer;
} }
// Helper method to process the upload with enhanced uploader // Helper method to process the upload with enhanced uploader
static Completer<SnCloudFile?> _processUploadWithEnhancedUploader( static Completer<SnCloudFile?> _processUpload(
UniversalFile fileData, UniversalFile fileData,
Dio client, WidgetRef ref,
String? poolId, String? poolId,
Function(double? progress, Duration estimate)? onProgress, Function(double? progress, Duration estimate)? onProgress,
Completer<SnCloudFile?> completer, Completer<SnCloudFile?> completer,
@@ -321,11 +311,11 @@ class FileUploader {
final data = fileData.data; final data = fileData.data;
if (data is XFile) { if (data is XFile) {
_performUploadWithEnhancedUploader( _performUpload(
fileData: data, fileData: data,
fileName: fileData.displayName ?? data.name, fileName: fileData.displayName ?? data.name,
contentType: actualMimetype, contentType: actualMimetype,
client: client, ref: ref,
poolId: poolId, poolId: poolId,
onProgress: onProgress, onProgress: onProgress,
completer: completer, completer: completer,
@@ -348,11 +338,11 @@ class FileUploader {
} }
if (bytes != null) { if (bytes != null) {
_performUploadWithEnhancedUploader( _performUpload(
fileData: bytes, fileData: bytes,
fileName: actualFilename, fileName: actualFilename,
contentType: actualMimetype, contentType: actualMimetype,
client: client, ref: ref,
poolId: poolId, poolId: poolId,
onProgress: onProgress, onProgress: onProgress,
completer: completer, completer: completer,
@@ -362,17 +352,18 @@ class FileUploader {
return completer; return completer;
} }
// Helper method to perform the actual upload // Helper method to perform the actual upload with enhanced uploader
static void _performUpload({ static void _performUpload({
required dynamic fileData, required dynamic fileData,
required String fileName, required String fileName,
required String contentType, required String contentType,
required Dio client, required WidgetRef ref,
String? poolId, String? poolId,
Function(double? progress, Duration estimate)? onProgress, Function(double? progress, Duration estimate)? onProgress,
required Completer<SnCloudFile?> completer, required Completer<SnCloudFile?> completer,
}) { }) {
final uploader = FileUploader(client); // Use the enhanced uploader with task tracking
final uploader = ref.read(enhancedFileUploaderProvider);
// Call progress start // Call progress start
onProgress?.call(null, Duration.zero); onProgress?.call(null, Duration.zero);
@@ -395,30 +386,6 @@ class FileUploader {
}); });
} }
// Helper method to perform the actual upload with enhanced uploader
static void _performUploadWithEnhancedUploader({
required dynamic fileData,
required String fileName,
required String contentType,
required Dio client,
String? poolId,
Function(double? progress, Duration estimate)? onProgress,
required Completer<SnCloudFile?> completer,
}) {
// Use the enhanced uploader from Riverpod context
// This will be called from a context where we have access to Riverpod
// For now, fall back to the regular uploader
_performUpload(
fileData: fileData,
fileName: fileName,
contentType: contentType,
client: client,
poolId: poolId,
onProgress: onProgress,
completer: completer,
);
}
/// Gets the MIME type of a UniversalFile. /// Gets the MIME type of a UniversalFile.
static String getMimeType(UniversalFile file, {bool useFallback = true}) { static String getMimeType(UniversalFile file, {bool useFallback = true}) {
final data = file.data; final data = file.data;

View File

@@ -401,7 +401,7 @@ class AttachmentPreview extends HookConsumerWidget {
children: [ children: [
if (progress != null) if (progress != null)
Text( Text(
'${progress!.toStringAsFixed(2)}%', '${(progress! * 100).toStringAsFixed(2)}%',
style: TextStyle(color: Colors.white), style: TextStyle(color: Colors.white),
) )
else else

View File

@@ -6,7 +6,6 @@ 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/network.dart';
import 'package:island/services/file_uploader.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';
@@ -61,7 +60,7 @@ class CloudFilePicker extends HookConsumerWidget {
final cloudFile = final cloudFile =
await FileUploader.createCloudFile( await FileUploader.createCloudFile(
fileData: file, fileData: file,
client: ref.read(apiClientProvider), ref: ref,
onProgress: (progress, _) { onProgress: (progress, _) {
uploadProgress.value = progress; uploadProgress.value = progress;
}, },

View File

@@ -180,7 +180,7 @@ class ComposeLogic {
try { try {
final cloudFile = final cloudFile =
await FileUploader.createCloudFile( await FileUploader.createCloudFile(
client: ref.read(apiClientProvider), ref: ref,
fileData: attachment, fileData: attachment,
).future; ).future;
if (cloudFile != null) { if (cloudFile != null) {
@@ -510,7 +510,7 @@ class ComposeLogic {
cloudFile = cloudFile =
await FileUploader.createCloudFile( await FileUploader.createCloudFile(
client: ref.read(apiClientProvider), ref: ref,
fileData: attachment, fileData: attachment,
poolId: poolId ?? selectedPoolId, poolId: poolId ?? selectedPoolId,
mode: mode:

View File

@@ -241,7 +241,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
final file = universalFiles[idx]; final file = universalFiles[idx];
final cloudFile = final cloudFile =
await FileUploader.createCloudFile( await FileUploader.createCloudFile(
client: apiClient, ref: ref,
fileData: file, fileData: file,
onProgress: (progress, _) { onProgress: (progress, _) {
if (mounted) { if (mounted) {

View File

@@ -18,30 +18,190 @@ class UploadOverlay extends HookConsumerWidget {
(task) => (task) =>
task.status == UploadTaskStatus.pending || task.status == UploadTaskStatus.pending ||
task.status == UploadTaskStatus.inProgress || task.status == UploadTaskStatus.inProgress ||
task.status == UploadTaskStatus.paused, task.status == UploadTaskStatus.paused ||
task.status == UploadTaskStatus.completed,
) )
.toList(); .toList();
// if (activeTasks.isEmpty) {
// return const SizedBox.shrink();
// }
if (activeTasks.isEmpty) { return _UploadOverlayContent(activeTasks: activeTasks);
return const SizedBox.shrink(); }
} }
class _UploadOverlayContent extends HookConsumerWidget {
final List<UploadTask> activeTasks;
const _UploadOverlayContent({required this.activeTasks});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isExpanded = useState(false);
final animationController = useAnimationController(
duration: const Duration(milliseconds: 200),
initialValue: 0.0,
);
final heightAnimation = useAnimation(
Tween<double>(begin: 60, end: 400).animate(
CurvedAnimation(parent: animationController, curve: Curves.easeInOut),
),
);
final opacityAnimation = useAnimation(
CurvedAnimation(parent: animationController, curve: Curves.easeInOut),
);
useEffect(() {
if (isExpanded.value) {
animationController.forward();
} else {
animationController.reverse();
}
return null;
}, [isExpanded.value]);
final isMobile = MediaQuery.of(context).size.width < 600;
return Positioned( return Positioned(
bottom: 16, bottom: isMobile ? 16 : 24,
right: 16, left: isMobile ? 16 : null,
child: Material( right: isMobile ? 16 : 24,
elevation: 8, child: GestureDetector(
onTap: () => isExpanded.value = !isExpanded.value,
child: AnimatedBuilder(
animation: animationController,
builder: (context, child) {
return Material(
elevation: 8 + (opacityAnimation * 4),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: Container( child: AnimatedContainer(
width: 320, duration: const Duration(milliseconds: 200),
constraints: BoxConstraints(maxHeight: 400), curve: Curves.easeInOut,
width: isMobile ? MediaQuery.of(context).size.width - 32 : 320,
height: heightAnimation,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Header // Collapsed Header
Container( Container(
padding: const EdgeInsets.all(16), height: 60,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
// Upload icon with animation
AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: child,
);
},
child: Icon(
key: ValueKey(isExpanded.value),
isExpanded.value
? Symbols.expand_more
: Symbols.upload,
size: 24,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(width: 12),
// Title and count
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isExpanded.value
? 'uploadTasks'.tr()
: '${activeTasks.length} ${'uploading'.tr()}',
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(fontWeight: FontWeight.w600),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (!isExpanded.value &&
activeTasks.isNotEmpty)
Text(
_getOverallProgressText(activeTasks),
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
],
),
),
// Progress indicator (collapsed)
if (!isExpanded.value)
SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator(
value: _getOverallProgress(activeTasks),
strokeWidth: 3,
backgroundColor:
Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
),
// Expand/collapse button
IconButton(
icon: AnimatedRotation(
turns: opacityAnimation * 0.5,
duration: const Duration(milliseconds: 200),
child: Icon(
isExpanded.value
? Symbols.expand_more
: Symbols.chevron_right,
size: 20,
),
),
onPressed:
() => isExpanded.value = !isExpanded.value,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
),
// Expanded content
if (isExpanded.value)
Expanded(
child: Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
),
),
child: Column(
children: [
// Clear completed tasks button
if (_hasCompletedTasks(activeTasks))
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
bottom: BorderSide( bottom: BorderSide(
@@ -51,25 +211,26 @@ class UploadOverlay extends HookConsumerWidget {
), ),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
Icon( TextButton.icon(
Symbols.upload, onPressed: () {
size: 20, ref
color: Theme.of(context).colorScheme.primary, .read(
uploadTasksProvider.notifier,
)
.clearCompletedTasks();
},
icon: Icon(
Symbols.clear_all,
size: 18,
), ),
const SizedBox(width: 8), label: const Text('Clear Completed'),
Text( style: TextButton.styleFrom(
'uploadTasks'.tr(), foregroundColor:
style: Theme.of(context).textTheme.titleMedium?.copyWith( Theme.of(
fontWeight: FontWeight.w600, context,
), ).colorScheme.onSurfaceVariant,
),
const Spacer(),
Text(
'${activeTasks.length}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w600,
), ),
), ),
], ],
@@ -77,9 +238,13 @@ class UploadOverlay extends HookConsumerWidget {
), ),
// Task list // Task list
Flexible( Expanded(
child: AnimatedOpacity(
opacity: opacityAnimation,
duration: const Duration(milliseconds: 150),
child: ListView.builder( child: ListView.builder(
shrinkWrap: true, shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: activeTasks.length, itemCount: activeTasks.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final task = activeTasks[index]; final task = activeTasks[index];
@@ -87,10 +252,43 @@ class UploadOverlay extends HookConsumerWidget {
}, },
), ),
), ),
),
], ],
), ),
), ),
), ),
],
),
),
),
);
},
),
),
);
}
double _getOverallProgress(List<UploadTask> tasks) {
if (tasks.isEmpty) return 0.0;
final totalProgress = tasks.fold<double>(
0.0,
(sum, task) => sum + task.progress,
);
return totalProgress / tasks.length;
}
String _getOverallProgressText(List<UploadTask> tasks) {
final overallProgress = _getOverallProgress(tasks);
return '${(overallProgress * 100).toStringAsFixed(0)}%';
}
bool _hasCompletedTasks(List<UploadTask> tasks) {
return tasks.any(
(task) =>
task.status == UploadTaskStatus.completed ||
task.status == UploadTaskStatus.failed ||
task.status == UploadTaskStatus.cancelled ||
task.status == UploadTaskStatus.expired,
); );
} }
} }
@@ -219,6 +417,8 @@ class UploadTaskTile extends HookConsumerWidget {
} }
Widget _buildExpandedDetails(BuildContext context) { Widget _buildExpandedDetails(BuildContext context) {
final transmissionProgress = task.transmissionProgress ?? 0.0;
return Container( return Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -228,7 +428,15 @@ class UploadTaskTile extends HookConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Progress text // Server Processing Progress
Text(
'Server Processing',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 2),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@@ -244,10 +452,7 @@ class UploadTaskTile extends HookConsumerWidget {
), ),
], ],
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
// Progress bar
LinearProgressIndicator( LinearProgressIndicator(
value: task.progress, value: task.progress,
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
@@ -256,6 +461,41 @@ class UploadTaskTile extends HookConsumerWidget {
), ),
), ),
const SizedBox(height: 8),
// File Transmission Progress
Text(
'File Transmission',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.secondary,
),
),
const SizedBox(height: 2),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${(transmissionProgress * 100).toStringAsFixed(1)}%',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
),
Text(
'${_formatFileSize((transmissionProgress * task.fileSize).toInt())} / ${_formatFileSize(task.fileSize)}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: transmissionProgress,
backgroundColor: Theme.of(context).colorScheme.surface,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.secondary,
),
),
const SizedBox(height: 4), const SizedBox(height: 4),
// Speed and ETA // Speed and ETA