♻️ Refactored the upload files according to new backend tasks

This commit is contained in:
2025-11-09 14:09:26 +08:00
parent e4019dadc8
commit 4927795260
10 changed files with 1149 additions and 10 deletions

View File

@@ -1087,6 +1087,7 @@
"levelingStage10": "Immortal", "levelingStage10": "Immortal",
"levelingStage11": "Divine", "levelingStage11": "Divine",
"levelingStage12": "Transcendent", "levelingStage12": "Transcendent",
"uploadTasks": "Upload Tasks",
"uploadAttachment": "Upload Attachment", "uploadAttachment": "Upload Attachment",
"attachmentPreview": "Attachment Preview", "attachmentPreview": "Attachment Preview",
"selectPool": "Select Pool", "selectPool": "Select Pool",

View File

@@ -0,0 +1,54 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart';
part 'upload_task.freezed.dart';
part 'upload_task.g.dart';
enum UploadTaskStatus {
pending,
inProgress,
paused,
completed,
failed,
expired,
cancelled,
}
@freezed
sealed class UploadTask with _$UploadTask {
const UploadTask._();
const factory UploadTask({
required String id,
required String taskId,
required String fileName,
required String contentType,
required int fileSize,
required int uploadedBytes,
required int totalChunks,
required int uploadedChunks,
required UploadTaskStatus status,
required DateTime createdAt,
required DateTime updatedAt,
String? errorMessage,
SnCloudFile? result,
String? poolId,
String? bundleId,
String? encryptPassword,
String? expiredAt,
}) = _UploadTask;
factory UploadTask.fromJson(Map<String, dynamic> json) =>
_$UploadTaskFromJson(json);
double get progress => totalChunks > 0 ? uploadedChunks / totalChunks : 0.0;
Duration get estimatedTimeRemaining {
if (uploadedBytes == 0 || fileSize == 0) return Duration.zero;
final remainingBytes = fileSize - uploadedBytes;
final uploadRate =
uploadedBytes / createdAt.difference(DateTime.now()).inSeconds.abs();
if (uploadRate == 0) return Duration.zero;
return Duration(seconds: (remainingBytes / uploadRate).round());
}
}

View File

@@ -0,0 +1,343 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'upload_task.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
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;
/// Create a copy of UploadTask
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$UploadTaskCopyWith<UploadTask> get copyWith => _$UploadTaskCopyWithImpl<UploadTask>(this as UploadTask, _$identity);
/// Serializes this UploadTask to a JSON map.
Map<String, dynamic> toJson();
@override
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));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,taskId,fileName,contentType,fileSize,uploadedBytes,totalChunks,uploadedChunks,status,createdAt,updatedAt,errorMessage,result,poolId,bundleId,encryptPassword,expiredAt);
@override
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)';
}
}
/// @nodoc
abstract mixin class $UploadTaskCopyWith<$Res> {
factory $UploadTaskCopyWith(UploadTask value, $Res Function(UploadTask) _then) = _$UploadTaskCopyWithImpl;
@useResult
$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
});
$SnCloudFileCopyWith<$Res>? get result;
}
/// @nodoc
class _$UploadTaskCopyWithImpl<$Res>
implements $UploadTaskCopyWith<$Res> {
_$UploadTaskCopyWithImpl(this._self, this._then);
final UploadTask _self;
final $Res Function(UploadTask) _then;
/// Create a copy of UploadTask
/// 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,}) {
return _then(_self.copyWith(
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,fileName: null == fileName ? _self.fileName : fileName // ignore: cast_nullable_to_non_nullable
as String,contentType: null == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
as String,fileSize: null == fileSize ? _self.fileSize : fileSize // ignore: cast_nullable_to_non_nullable
as int,uploadedBytes: null == uploadedBytes ? _self.uploadedBytes : uploadedBytes // ignore: cast_nullable_to_non_nullable
as int,totalChunks: null == totalChunks ? _self.totalChunks : totalChunks // ignore: cast_nullable_to_non_nullable
as int,uploadedChunks: null == uploadedChunks ? _self.uploadedChunks : uploadedChunks // 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 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 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 String?,bundleId: freezed == bundleId ? _self.bundleId : bundleId // ignore: cast_nullable_to_non_nullable
as String?,encryptPassword: freezed == encryptPassword ? _self.encryptPassword : encryptPassword // ignore: cast_nullable_to_non_nullable
as String?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as String?,
));
}
/// Create a copy of UploadTask
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get result {
if (_self.result == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.result!, (value) {
return _then(_self.copyWith(result: value));
});
}
}
/// Adds pattern-matching-related methods to [UploadTask].
extension UploadTaskPatterns on UploadTask {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _UploadTask value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _UploadTask() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _UploadTask value) $default,){
final _that = this;
switch (_that) {
case _UploadTask():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _UploadTask value)? $default,){
final _that = this;
switch (_that) {
case _UploadTask() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@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;
switch (_that) {
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 orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@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;
switch (_that) {
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);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@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;
switch (_that) {
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 null;
}
}
}
/// @nodoc
@JsonSerializable()
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._();
factory _UploadTask.fromJson(Map<String, dynamic> json) => _$UploadTaskFromJson(json);
@override final String id;
@override final String taskId;
@override final String fileName;
@override final String contentType;
@override final int fileSize;
@override final int uploadedBytes;
@override final int totalChunks;
@override final int uploadedChunks;
@override final UploadTaskStatus status;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final String? errorMessage;
@override final SnCloudFile? result;
@override final String? poolId;
@override final String? bundleId;
@override final String? encryptPassword;
@override final String? expiredAt;
/// Create a copy of UploadTask
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$UploadTaskCopyWith<_UploadTask> get copyWith => __$UploadTaskCopyWithImpl<_UploadTask>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$UploadTaskToJson(this, );
}
@override
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));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,taskId,fileName,contentType,fileSize,uploadedBytes,totalChunks,uploadedChunks,status,createdAt,updatedAt,errorMessage,result,poolId,bundleId,encryptPassword,expiredAt);
@override
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)';
}
}
/// @nodoc
abstract mixin class _$UploadTaskCopyWith<$Res> implements $UploadTaskCopyWith<$Res> {
factory _$UploadTaskCopyWith(_UploadTask value, $Res Function(_UploadTask) _then) = __$UploadTaskCopyWithImpl;
@override @useResult
$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
});
@override $SnCloudFileCopyWith<$Res>? get result;
}
/// @nodoc
class __$UploadTaskCopyWithImpl<$Res>
implements _$UploadTaskCopyWith<$Res> {
__$UploadTaskCopyWithImpl(this._self, this._then);
final _UploadTask _self;
final $Res Function(_UploadTask) _then;
/// Create a copy of UploadTask
/// 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,}) {
return _then(_UploadTask(
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,fileName: null == fileName ? _self.fileName : fileName // ignore: cast_nullable_to_non_nullable
as String,contentType: null == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
as String,fileSize: null == fileSize ? _self.fileSize : fileSize // ignore: cast_nullable_to_non_nullable
as int,uploadedBytes: null == uploadedBytes ? _self.uploadedBytes : uploadedBytes // ignore: cast_nullable_to_non_nullable
as int,totalChunks: null == totalChunks ? _self.totalChunks : totalChunks // ignore: cast_nullable_to_non_nullable
as int,uploadedChunks: null == uploadedChunks ? _self.uploadedChunks : uploadedChunks // 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 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 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 String?,bundleId: freezed == bundleId ? _self.bundleId : bundleId // ignore: cast_nullable_to_non_nullable
as String?,encryptPassword: freezed == encryptPassword ? _self.encryptPassword : encryptPassword // ignore: cast_nullable_to_non_nullable
as String?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as String?,
));
}
/// Create a copy of UploadTask
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get result {
if (_self.result == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.result!, (value) {
return _then(_self.copyWith(result: value));
});
}
}
// dart format on

View File

@@ -0,0 +1,61 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'upload_task.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_UploadTask _$UploadTaskFromJson(Map<String, dynamic> json) => _UploadTask(
id: json['id'] as String,
taskId: json['task_id'] as String,
fileName: json['file_name'] as String,
contentType: json['content_type'] as String,
fileSize: (json['file_size'] as num).toInt(),
uploadedBytes: (json['uploaded_bytes'] as num).toInt(),
totalChunks: (json['total_chunks'] as num).toInt(),
uploadedChunks: (json['uploaded_chunks'] as num).toInt(),
status: $enumDecode(_$UploadTaskStatusEnumMap, json['status']),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
errorMessage: json['error_message'] as String?,
result:
json['result'] == null
? null
: SnCloudFile.fromJson(json['result'] as Map<String, dynamic>),
poolId: json['pool_id'] as String?,
bundleId: json['bundle_id'] as String?,
encryptPassword: json['encrypt_password'] as String?,
expiredAt: json['expired_at'] as String?,
);
Map<String, dynamic> _$UploadTaskToJson(_UploadTask instance) =>
<String, dynamic>{
'id': instance.id,
'task_id': instance.taskId,
'file_name': instance.fileName,
'content_type': instance.contentType,
'file_size': instance.fileSize,
'uploaded_bytes': instance.uploadedBytes,
'total_chunks': instance.totalChunks,
'uploaded_chunks': instance.uploadedChunks,
'status': _$UploadTaskStatusEnumMap[instance.status]!,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'error_message': instance.errorMessage,
'result': instance.result?.toJson(),
'pool_id': instance.poolId,
'bundle_id': instance.bundleId,
'encrypt_password': instance.encryptPassword,
'expired_at': instance.expiredAt,
};
const _$UploadTaskStatusEnumMap = {
UploadTaskStatus.pending: 'pending',
UploadTaskStatus.inProgress: 'inProgress',
UploadTaskStatus.paused: 'paused',
UploadTaskStatus.completed: 'completed',
UploadTaskStatus.failed: 'failed',
UploadTaskStatus.expired: 'expired',
UploadTaskStatus.cancelled: 'cancelled',
};

View File

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

317
lib/pods/upload_tasks.dart Normal file
View File

@@ -0,0 +1,317 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:cross_file/cross_file.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/models/upload_task.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/services/file_uploader.dart';
final uploadTasksProvider =
StateNotifierProvider<UploadTasksNotifier, List<UploadTask>>(
(ref) => UploadTasksNotifier(ref),
);
class UploadTasksNotifier extends StateNotifier<List<UploadTask>> {
final Ref ref;
StreamSubscription? _websocketSubscription;
UploadTasksNotifier(this.ref) : super([]) {
_listenToWebSocket();
}
void _listenToWebSocket() {
final WebSocketService websocketService = ref.read(websocketProvider);
_websocketSubscription = websocketService.dataStream.listen(
_handleWebSocketPacket,
);
}
void _handleWebSocketPacket(dynamic packet) {
if (packet.type.startsWith('upload.')) {
final data = packet.data;
if (data == null) return;
final taskId = data['task_id'] as String?;
if (taskId == null) return;
switch (packet.type) {
case 'upload.progress':
_handleProgressUpdate(taskId, data);
break;
case 'upload.completed':
_handleUploadCompleted(taskId, data);
break;
case 'upload.failed':
_handleUploadFailed(taskId, data);
break;
}
}
}
void _handleProgressUpdate(String taskId, Map<String, dynamic> data) {
final uploadedChunks = data['chunksUploaded'] as int? ?? 0;
final uploadedBytes =
(data['progress'] as num? ?? 0.0) /
100.0 *
(data['fileSize'] as int? ?? 0);
state =
state.map((task) {
if (task.taskId == taskId) {
return task.copyWith(
uploadedChunks: uploadedChunks,
uploadedBytes: uploadedBytes.toInt(),
status: UploadTaskStatus.inProgress,
updatedAt: DateTime.now(),
);
}
return task;
}).toList();
}
void _handleUploadCompleted(String taskId, Map<String, dynamic> data) {
final fileData = data['file'];
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.map((task) {
if (task.taskId == taskId) {
return task.copyWith(
status: UploadTaskStatus.completed,
uploadedChunks: task.totalChunks,
uploadedBytes: task.fileSize,
updatedAt: DateTime.now(),
);
}
return task;
}).toList();
}
void _handleUploadFailed(String taskId, Map<String, dynamic> data) {
final errorMessage = data['error'] as String? ?? 'Upload failed';
state =
state.map((task) {
if (task.taskId == taskId) {
return task.copyWith(
status: UploadTaskStatus.failed,
errorMessage: errorMessage,
updatedAt: DateTime.now(),
);
}
return task;
}).toList();
}
void addUploadTask(UploadTask task) {
state = [...state, task];
}
void updateTaskStatus(
String taskId,
UploadTaskStatus status, {
String? errorMessage,
}) {
state =
state.map((task) {
if (task.taskId == taskId) {
return task.copyWith(
status: status,
errorMessage: errorMessage,
updatedAt: DateTime.now(),
);
}
return task;
}).toList();
}
void removeTask(String taskId) {
state = state.where((task) => task.taskId != taskId).toList();
}
UploadTask? getTask(String taskId) {
return state.where((task) => task.taskId == taskId).firstOrNull;
}
List<UploadTask> getActiveTasks() {
return state
.where(
(task) =>
task.status == UploadTaskStatus.pending ||
task.status == UploadTaskStatus.inProgress ||
task.status == UploadTaskStatus.paused,
)
.toList();
}
@override
void dispose() {
_websocketSubscription?.cancel();
super.dispose();
}
}
// Provider for the enhanced FileUploader that integrates with upload tasks
final enhancedFileUploaderProvider = Provider<EnhancedFileUploader>((ref) {
final dio = ref.watch(apiClientProvider);
return EnhancedFileUploader(dio, ref);
});
class EnhancedFileUploader extends FileUploader {
final Ref ref;
EnhancedFileUploader(super.client, this.ref);
/// Reads the next chunk from a stream subscription.
Future<Uint8List> _readNextChunkFromStream(
StreamSubscription<List<int>> subscription,
int size,
) async {
final completer = Completer<Uint8List>();
final buffer = <int>[];
int remaining = size;
void onData(List<int> data) {
buffer.addAll(data);
remaining -= data.length;
if (remaining <= 0) {
subscription.pause();
completer.complete(Uint8List.fromList(buffer.sublist(0, size)));
}
}
void onDone() {
if (!completer.isCompleted) {
completer.complete(Uint8List.fromList(buffer));
}
}
subscription.onData(onData);
subscription.onDone(onDone);
return completer.future;
}
@override
Future<SnCloudFile> uploadFile({
required dynamic fileData,
required String fileName,
required String contentType,
String? poolId,
String? bundleId,
String? encryptPassword,
String? expiredAt,
int? customChunkSize,
Function(double? progress, Duration estimate)? onProgress,
}) async {
// Step 1: Create upload task
onProgress?.call(null, Duration.zero);
final createResponse = await createUploadTask(
fileData: fileData,
fileName: fileName,
contentType: contentType,
poolId: poolId,
bundleId: bundleId,
encryptPassword: encryptPassword,
expiredAt: expiredAt,
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;
if (fileData is XFile) {
totalSize = await fileData.length();
} else if (fileData is Uint8List) {
totalSize = fileData.length;
} else {
throw ArgumentError('Invalid fileData type');
}
// Create upload task and add to state
final uploadTask = UploadTask(
id: DateTime.now().millisecondsSinceEpoch.toString(),
taskId: taskId,
fileName: fileName,
contentType: contentType,
fileSize: totalSize,
uploadedBytes: 0,
totalChunks: chunksCount,
uploadedChunks: 0,
status: UploadTaskStatus.pending,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
poolId: poolId,
bundleId: bundleId,
encryptPassword: encryptPassword,
expiredAt: expiredAt,
);
ref.read(uploadTasksProvider.notifier).addUploadTask(uploadTask);
// Step 2: Upload chunks
int bytesUploaded = 0;
if (fileData is XFile) {
// Use stream for XFile
final subscription = fileData.openRead().listen(null);
subscription.pause();
for (int i = 0; i < chunksCount; i++) {
subscription.resume();
final chunkData = await _readNextChunkFromStream(
subscription,
chunkSize,
);
await uploadChunk(
taskId: taskId,
chunkIndex: i,
chunkData: chunkData,
onSendProgress: (sent, total) {
final overallProgress = (bytesUploaded + sent) / totalSize;
onProgress?.call(overallProgress, Duration.zero);
},
);
bytesUploaded += chunkData.length;
}
subscription.cancel();
} else if (fileData is Uint8List) {
// Use old way for Uint8List
final chunks = <Uint8List>[];
for (int i = 0; i < fileData.length; i += chunkSize) {
final end =
i + chunkSize > fileData.length ? fileData.length : i + chunkSize;
chunks.add(Uint8List.fromList(fileData.sublist(i, end)));
}
// Upload each chunk
for (int i = 0; i < chunks.length; i++) {
await uploadChunk(
taskId: taskId,
chunkIndex: i,
chunkData: chunks[i],
onSendProgress: (sent, total) {
final overallProgress = (bytesUploaded + sent) / totalSize;
onProgress?.call(overallProgress, Duration.zero);
},
);
bytesUploaded += chunks[i].length;
}
} else {
throw ArgumentError('Invalid fileData type');
}
// Step 3: Complete upload
onProgress?.call(null, Duration.zero);
return await completeUpload(taskId);
}
}

View File

@@ -266,7 +266,7 @@ class FileUploader {
await exif.writeAttributes(gpsAttributes); await exif.writeAttributes(gpsAttributes);
}) })
.then( .then(
(_) => _processUpload( (_) => _processUploadWithEnhancedUploader(
fileData, fileData,
client, client,
poolId, poolId,
@@ -276,7 +276,7 @@ class FileUploader {
) )
.catchError((e) { .catchError((e) {
debugPrint('Error removing GPS EXIF data: $e'); debugPrint('Error removing GPS EXIF data: $e');
return _processUpload( return _processUploadWithEnhancedUploader(
fileData, fileData,
client, client,
poolId, poolId,
@@ -289,12 +289,18 @@ class FileUploader {
} }
} }
_processUpload(fileData, client, poolId, onProgress, completer); _processUploadWithEnhancedUploader(
fileData,
client,
poolId,
onProgress,
completer,
);
return completer; return completer;
} }
// Helper method to process the upload // Helper method to process the upload with enhanced uploader
static Completer<SnCloudFile?> _processUpload( static Completer<SnCloudFile?> _processUploadWithEnhancedUploader(
UniversalFile fileData, UniversalFile fileData,
Dio client, Dio client,
String? poolId, String? poolId,
@@ -309,7 +315,7 @@ class FileUploader {
final data = fileData.data; final data = fileData.data;
if (data is XFile) { if (data is XFile) {
_performUpload( _performUploadWithEnhancedUploader(
fileData: data, fileData: data,
fileName: fileData.displayName ?? data.name, fileName: fileData.displayName ?? data.name,
contentType: actualMimetype, contentType: actualMimetype,
@@ -336,7 +342,7 @@ class FileUploader {
} }
if (bytes != null) { if (bytes != null) {
_performUpload( _performUploadWithEnhancedUploader(
fileData: bytes, fileData: bytes,
fileName: actualFilename, fileName: actualFilename,
contentType: actualMimetype, contentType: actualMimetype,
@@ -383,6 +389,30 @@ 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

@@ -13,6 +13,7 @@ import 'package:island/route.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/upload_overlay.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@@ -198,6 +199,7 @@ class WindowScaffold extends HookConsumerWidget {
], ],
), ),
_WebSocketIndicator(), _WebSocketIndicator(),
const UploadOverlay(),
], ],
), ),
), ),
@@ -213,7 +215,11 @@ class WindowScaffold extends HookConsumerWidget {
actions: <Type, Action<Intent>>{PopIntent: PopAction(ref)}, actions: <Type, Action<Intent>>{PopIntent: PopAction(ref)},
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [Positioned.fill(child: child), _WebSocketIndicator()], children: [
Positioned.fill(child: child),
_WebSocketIndicator(),
const UploadOverlay(),
],
), ),
), ),
); );

View File

@@ -6,7 +6,7 @@ part of 'post_list.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$postListNotifierHash() => r'8241120dc3c2004387c6cf881e5cb9224cbd3a97'; String _$postListNotifierHash() => r'bfc3d652dffc5ff3a94a6c3d04aac65354fe63b5';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {

View File

@@ -0,0 +1,327 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/upload_task.dart';
import 'package:island/pods/upload_tasks.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:easy_localization/easy_localization.dart';
class UploadOverlay extends HookConsumerWidget {
const UploadOverlay({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final uploadTasks = ref.watch(uploadTasksProvider);
final activeTasks =
uploadTasks
.where(
(task) =>
task.status == UploadTaskStatus.pending ||
task.status == UploadTaskStatus.inProgress ||
task.status == UploadTaskStatus.paused,
)
.toList();
if (activeTasks.isEmpty) {
return const SizedBox.shrink();
}
return Positioned(
bottom: 16,
right: 16,
child: Material(
elevation: 8,
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.surfaceContainer,
child: Container(
width: 320,
constraints: BoxConstraints(maxHeight: 400),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
),
),
child: Row(
children: [
Icon(
Symbols.upload,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'uploadTasks'.tr(),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const Spacer(),
Text(
'${activeTasks.length}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
],
),
),
// Task list
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: activeTasks.length,
itemBuilder: (context, index) {
final task = activeTasks[index];
return UploadTaskTile(task: task);
},
),
),
],
),
),
),
);
}
}
class UploadTaskTile extends HookConsumerWidget {
final UploadTask task;
const UploadTaskTile({super.key, required this.task});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isExpanded = useState(false);
return InkWell(
onTap: () => isExpanded.value = !isExpanded.value,
child: Container(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
// Status icon
_buildStatusIcon(context),
const SizedBox(width: 8),
// File info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
task.fileName,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
_formatFileSize(task.fileSize),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
// Progress indicator
const SizedBox(width: 8),
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
value: task.progress,
strokeWidth: 3,
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerHighest,
),
),
// Expand/collapse button
IconButton(
icon: Icon(
isExpanded.value
? Symbols.expand_less
: Symbols.expand_more,
size: 16,
),
onPressed: () => isExpanded.value = !isExpanded.value,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
// Expanded details
if (isExpanded.value) ...[
const SizedBox(height: 8),
_buildExpandedDetails(context),
],
],
),
),
);
}
Widget _buildStatusIcon(BuildContext context) {
IconData icon;
Color color;
switch (task.status) {
case UploadTaskStatus.pending:
icon = Symbols.schedule;
color = Theme.of(context).colorScheme.secondary;
break;
case UploadTaskStatus.inProgress:
icon = Symbols.upload;
color = Theme.of(context).colorScheme.primary;
break;
case UploadTaskStatus.paused:
icon = Symbols.pause_circle;
color = Theme.of(context).colorScheme.tertiary;
break;
case UploadTaskStatus.completed:
icon = Symbols.check_circle;
color = Colors.green;
break;
case UploadTaskStatus.failed:
icon = Symbols.error;
color = Theme.of(context).colorScheme.error;
break;
case UploadTaskStatus.cancelled:
icon = Symbols.cancel;
color = Theme.of(context).colorScheme.error;
break;
case UploadTaskStatus.expired:
icon = Symbols.timer_off;
color = Theme.of(context).colorScheme.error;
break;
}
return Icon(icon, size: 16, color: color);
}
Widget _buildExpandedDetails(BuildContext context) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Progress text
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${(task.progress * 100).toStringAsFixed(1)}%',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
),
Text(
'${task.uploadedChunks}/${task.totalChunks} chunks',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
const SizedBox(height: 4),
// Progress bar
LinearProgressIndicator(
value: task.progress,
backgroundColor: Theme.of(context).colorScheme.surface,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 4),
// Speed and ETA
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_formatBytesPerSecond(task),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
if (task.status == UploadTaskStatus.inProgress)
Text(
'ETA: ${_formatDuration(task.estimatedTimeRemaining)}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
// Error message if failed
if (task.errorMessage != null) ...[
const SizedBox(height: 4),
Text(
task.errorMessage!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
],
],
),
);
}
String _formatFileSize(int bytes) {
if (bytes >= 1073741824) {
return '${(bytes / 1073741824).toStringAsFixed(1)} GB';
} else if (bytes >= 1048576) {
return '${(bytes / 1048576).toStringAsFixed(1)} MB';
} else if (bytes >= 1024) {
return '${(bytes / 1024).toStringAsFixed(1)} KB';
} else {
return '$bytes bytes';
}
}
String _formatBytesPerSecond(UploadTask task) {
if (task.uploadedBytes == 0) return '0 B/s';
final elapsedSeconds = DateTime.now().difference(task.createdAt).inSeconds;
if (elapsedSeconds == 0) return '0 B/s';
final bytesPerSecond = task.uploadedBytes / elapsedSeconds;
return '${_formatFileSize(bytesPerSecond.toInt())}/s';
}
String _formatDuration(Duration duration) {
if (duration.inHours > 0) {
return '${duration.inHours}h ${duration.inMinutes.remainder(60)}m';
} else if (duration.inMinutes > 0) {
return '${duration.inMinutes}m ${duration.inSeconds.remainder(60)}s';
} else {
return '${duration.inSeconds}s';
}
}
}