Compare commits
13 Commits
3.3.0+145
...
0391893b32
| Author | SHA1 | Date | |
|---|---|---|---|
|
0391893b32
|
|||
|
b8d24876c8
|
|||
|
0493661f9a
|
|||
|
b40afde00f
|
|||
|
78a4022531
|
|||
|
8a291c80b7
|
|||
|
1395d65b76
|
|||
|
eb4942e0ed
|
|||
|
f254cfa81e
|
|||
|
4927795260
|
|||
|
e4019dadc8
|
|||
|
5e7d77e1a1
|
|||
|
bfcbed035c
|
@@ -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",
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Alamofire (5.10.2)
|
- Alamofire (5.10.2)
|
||||||
- app_links (6.4.1):
|
|
||||||
- Flutter
|
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- croppy (0.0.1):
|
- croppy (0.0.1):
|
||||||
@@ -52,18 +50,18 @@ PODS:
|
|||||||
- Firebase/Messaging (12.4.0):
|
- Firebase/Messaging (12.4.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseMessaging (~> 12.4.0)
|
- FirebaseMessaging (~> 12.4.0)
|
||||||
- firebase_analytics (12.0.3):
|
- firebase_analytics (12.0.4):
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- FirebaseAnalytics (= 12.4.0)
|
- FirebaseAnalytics (= 12.4.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_core (4.2.0):
|
- firebase_core (4.2.1):
|
||||||
- Firebase/CoreOnly (= 12.4.0)
|
- Firebase/CoreOnly (= 12.4.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_crashlytics (5.0.3):
|
- firebase_crashlytics (5.0.4):
|
||||||
- Firebase/Crashlytics (= 12.4.0)
|
- Firebase/Crashlytics (= 12.4.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_messaging (16.0.3):
|
- firebase_messaging (16.0.4):
|
||||||
- Firebase/Messaging (= 12.4.0)
|
- Firebase/Messaging (= 12.4.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -265,6 +263,8 @@ PODS:
|
|||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- PromisesSwift (2.4.0):
|
- PromisesSwift (2.4.0):
|
||||||
- PromisesObjC (= 2.4.0)
|
- PromisesObjC (= 2.4.0)
|
||||||
|
- protocol_handler_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
- receive_sharing_intent (1.8.1):
|
- receive_sharing_intent (1.8.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- record_ios (1.1.0):
|
- record_ios (1.1.0):
|
||||||
@@ -323,7 +323,6 @@ PODS:
|
|||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- Alamofire
|
- Alamofire
|
||||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||||
- croppy (from `.symlinks/plugins/croppy/ios`)
|
- croppy (from `.symlinks/plugins/croppy/ios`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
@@ -358,6 +357,7 @@ DEPENDENCIES:
|
|||||||
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
|
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
|
||||||
|
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
|
||||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||||
- record_ios (from `.symlinks/plugins/record_ios/ios`)
|
- record_ios (from `.symlinks/plugins/record_ios/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
@@ -404,8 +404,6 @@ SPEC REPOS:
|
|||||||
- WebRTC-SDK
|
- WebRTC-SDK
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
app_links:
|
|
||||||
:path: ".symlinks/plugins/app_links/ios"
|
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||||
croppy:
|
croppy:
|
||||||
@@ -470,6 +468,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
pointer_interceptor_ios:
|
pointer_interceptor_ios:
|
||||||
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
||||||
|
protocol_handler_ios:
|
||||||
|
:path: ".symlinks/plugins/protocol_handler_ios/ios"
|
||||||
receive_sharing_intent:
|
receive_sharing_intent:
|
||||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||||
record_ios:
|
record_ios:
|
||||||
@@ -497,7 +497,6 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||||
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
|
|
||||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||||
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
||||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||||
@@ -506,10 +505,10 @@ SPEC CHECKSUMS:
|
|||||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||||
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||||
firebase_analytics: 1d024068b1d4707d5ba7a42a12976ddf3316d835
|
firebase_analytics: 67fbdd9f3c04e55048024f3da21cfc36f05e56cf
|
||||||
firebase_core: 744984dbbed8b3036abf34f0b98d80f130a7e464
|
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
|
||||||
firebase_crashlytics: f3a9a4338ab99b67042f64e9e22e1bf349cb44ed
|
firebase_crashlytics: 83c7467d7534975a4d779af43bd226d0a4616464
|
||||||
firebase_messaging: 82c70650c426a0a14873e1acdb9ec2b443c4e8b4
|
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
|
||||||
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||||
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||||
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018
|
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018
|
||||||
@@ -553,6 +552,7 @@ SPEC CHECKSUMS:
|
|||||||
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||||
|
protocol_handler_ios: 59f23ee71f3ec602d67902ca7f669a80957888d5
|
||||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||||
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ void main() async {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await EasyLocalization.ensureInitialized();
|
await EasyLocalization.ensureInitialized();
|
||||||
|
// Disable logs
|
||||||
|
EasyLocalization.logger.enableBuildModes = [];
|
||||||
|
|
||||||
if (kIsWeb || !Platform.isLinux) {
|
if (kIsWeb || !Platform.isLinux) {
|
||||||
await Firebase.initializeApp(
|
await Firebase.initializeApp(
|
||||||
|
|||||||
57
lib/models/drive_task.dart
Normal file
57
lib/models/drive_task.dart
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
|
|
||||||
|
part 'drive_task.freezed.dart';
|
||||||
|
part 'drive_task.g.dart';
|
||||||
|
|
||||||
|
enum DriveTaskStatus {
|
||||||
|
pending,
|
||||||
|
inProgress,
|
||||||
|
paused,
|
||||||
|
completed,
|
||||||
|
failed,
|
||||||
|
expired,
|
||||||
|
cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class DriveTask with _$DriveTask {
|
||||||
|
const DriveTask._();
|
||||||
|
|
||||||
|
const factory DriveTask({
|
||||||
|
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 DriveTaskStatus status,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required String type, // Task type (e.g., 'FileUpload')
|
||||||
|
double? transmissionProgress, // Local file upload progress (0.0-1.0)
|
||||||
|
String? errorMessage,
|
||||||
|
String? statusMessage,
|
||||||
|
SnCloudFile? result,
|
||||||
|
String? poolId,
|
||||||
|
String? bundleId,
|
||||||
|
String? encryptPassword,
|
||||||
|
String? expiredAt,
|
||||||
|
}) = _DriveTask;
|
||||||
|
|
||||||
|
factory DriveTask.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$DriveTaskFromJson(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());
|
||||||
|
}
|
||||||
|
}
|
||||||
356
lib/models/drive_task.freezed.dart
Normal file
356
lib/models/drive_task.freezed.dart
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
// 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 'drive_task.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$DriveTask {
|
||||||
|
|
||||||
|
String get id; String get taskId; String get fileName; String get contentType; int get fileSize; int get uploadedBytes; int get totalChunks; int get uploadedChunks; DriveTaskStatus get status; DateTime get createdAt; DateTime get updatedAt; String get type;// Task type (e.g., 'FileUpload')
|
||||||
|
double? get transmissionProgress;// Local file upload progress (0.0-1.0)
|
||||||
|
String? get errorMessage; String? get statusMessage; SnCloudFile? get result; String? get poolId; String? get bundleId; String? get encryptPassword; String? get expiredAt;
|
||||||
|
/// Create a copy of DriveTask
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$DriveTaskCopyWith<DriveTask> get copyWith => _$DriveTaskCopyWithImpl<DriveTask>(this as DriveTask, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this DriveTask to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is DriveTask&&(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.type, type) || other.type == type)&&(identical(other.transmissionProgress, transmissionProgress) || other.transmissionProgress == transmissionProgress)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.statusMessage, statusMessage) || other.statusMessage == statusMessage)&&(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.hashAll([runtimeType,id,taskId,fileName,contentType,fileSize,uploadedBytes,totalChunks,uploadedChunks,status,createdAt,updatedAt,type,transmissionProgress,errorMessage,statusMessage,result,poolId,bundleId,encryptPassword,expiredAt]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'DriveTask(id: $id, taskId: $taskId, fileName: $fileName, contentType: $contentType, fileSize: $fileSize, uploadedBytes: $uploadedBytes, totalChunks: $totalChunks, uploadedChunks: $uploadedChunks, status: $status, createdAt: $createdAt, updatedAt: $updatedAt, type: $type, transmissionProgress: $transmissionProgress, errorMessage: $errorMessage, statusMessage: $statusMessage, result: $result, poolId: $poolId, bundleId: $bundleId, encryptPassword: $encryptPassword, expiredAt: $expiredAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $DriveTaskCopyWith<$Res> {
|
||||||
|
factory $DriveTaskCopyWith(DriveTask value, $Res Function(DriveTask) _then) = _$DriveTaskCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, DriveTaskStatus status, DateTime createdAt, DateTime updatedAt, String type, double? transmissionProgress, String? errorMessage, String? statusMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$SnCloudFileCopyWith<$Res>? get result;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$DriveTaskCopyWithImpl<$Res>
|
||||||
|
implements $DriveTaskCopyWith<$Res> {
|
||||||
|
_$DriveTaskCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final DriveTask _self;
|
||||||
|
final $Res Function(DriveTask) _then;
|
||||||
|
|
||||||
|
/// Create a copy of DriveTask
|
||||||
|
/// 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? type = null,Object? transmissionProgress = freezed,Object? errorMessage = freezed,Object? statusMessage = 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 DriveTaskStatus,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,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,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?,statusMessage: freezed == statusMessage ? _self.statusMessage : statusMessage // 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 DriveTask
|
||||||
|
/// 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 [DriveTask].
|
||||||
|
extension DriveTaskPatterns on DriveTask {
|
||||||
|
/// 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( _DriveTask value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _DriveTask() 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( _DriveTask value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _DriveTask():
|
||||||
|
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( _DriveTask value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _DriveTask() 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, DriveTaskStatus status, DateTime createdAt, DateTime updatedAt, String type, double? transmissionProgress, String? errorMessage, String? statusMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _DriveTask() 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.type,_that.transmissionProgress,_that.errorMessage,_that.statusMessage,_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, DriveTaskStatus status, DateTime createdAt, DateTime updatedAt, String type, double? transmissionProgress, String? errorMessage, String? statusMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _DriveTask():
|
||||||
|
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.type,_that.transmissionProgress,_that.errorMessage,_that.statusMessage,_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, DriveTaskStatus status, DateTime createdAt, DateTime updatedAt, String type, double? transmissionProgress, String? errorMessage, String? statusMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _DriveTask() 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.type,_that.transmissionProgress,_that.errorMessage,_that.statusMessage,_that.result,_that.poolId,_that.bundleId,_that.encryptPassword,_that.expiredAt);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _DriveTask extends DriveTask {
|
||||||
|
const _DriveTask({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, required this.type, this.transmissionProgress, this.errorMessage, this.statusMessage, this.result, this.poolId, this.bundleId, this.encryptPassword, this.expiredAt}): super._();
|
||||||
|
factory _DriveTask.fromJson(Map<String, dynamic> json) => _$DriveTaskFromJson(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 DriveTaskStatus status;
|
||||||
|
@override final DateTime createdAt;
|
||||||
|
@override final DateTime updatedAt;
|
||||||
|
@override final String type;
|
||||||
|
// Task type (e.g., 'FileUpload')
|
||||||
|
@override final double? transmissionProgress;
|
||||||
|
// Local file upload progress (0.0-1.0)
|
||||||
|
@override final String? errorMessage;
|
||||||
|
@override final String? statusMessage;
|
||||||
|
@override final SnCloudFile? result;
|
||||||
|
@override final String? poolId;
|
||||||
|
@override final String? bundleId;
|
||||||
|
@override final String? encryptPassword;
|
||||||
|
@override final String? expiredAt;
|
||||||
|
|
||||||
|
/// Create a copy of DriveTask
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$DriveTaskCopyWith<_DriveTask> get copyWith => __$DriveTaskCopyWithImpl<_DriveTask>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$DriveTaskToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DriveTask&&(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.type, type) || other.type == type)&&(identical(other.transmissionProgress, transmissionProgress) || other.transmissionProgress == transmissionProgress)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.statusMessage, statusMessage) || other.statusMessage == statusMessage)&&(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.hashAll([runtimeType,id,taskId,fileName,contentType,fileSize,uploadedBytes,totalChunks,uploadedChunks,status,createdAt,updatedAt,type,transmissionProgress,errorMessage,statusMessage,result,poolId,bundleId,encryptPassword,expiredAt]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'DriveTask(id: $id, taskId: $taskId, fileName: $fileName, contentType: $contentType, fileSize: $fileSize, uploadedBytes: $uploadedBytes, totalChunks: $totalChunks, uploadedChunks: $uploadedChunks, status: $status, createdAt: $createdAt, updatedAt: $updatedAt, type: $type, transmissionProgress: $transmissionProgress, errorMessage: $errorMessage, statusMessage: $statusMessage, result: $result, poolId: $poolId, bundleId: $bundleId, encryptPassword: $encryptPassword, expiredAt: $expiredAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$DriveTaskCopyWith<$Res> implements $DriveTaskCopyWith<$Res> {
|
||||||
|
factory _$DriveTaskCopyWith(_DriveTask value, $Res Function(_DriveTask) _then) = __$DriveTaskCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, DriveTaskStatus status, DateTime createdAt, DateTime updatedAt, String type, double? transmissionProgress, String? errorMessage, String? statusMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override $SnCloudFileCopyWith<$Res>? get result;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$DriveTaskCopyWithImpl<$Res>
|
||||||
|
implements _$DriveTaskCopyWith<$Res> {
|
||||||
|
__$DriveTaskCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _DriveTask _self;
|
||||||
|
final $Res Function(_DriveTask) _then;
|
||||||
|
|
||||||
|
/// Create a copy of DriveTask
|
||||||
|
/// 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? type = null,Object? transmissionProgress = freezed,Object? errorMessage = freezed,Object? statusMessage = freezed,Object? result = freezed,Object? poolId = freezed,Object? bundleId = freezed,Object? encryptPassword = freezed,Object? expiredAt = freezed,}) {
|
||||||
|
return _then(_DriveTask(
|
||||||
|
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 DriveTaskStatus,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,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,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?,statusMessage: freezed == statusMessage ? _self.statusMessage : statusMessage // 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 DriveTask
|
||||||
|
/// 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
|
||||||
67
lib/models/drive_task.g.dart
Normal file
67
lib/models/drive_task.g.dart
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'drive_task.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_DriveTask _$DriveTaskFromJson(Map<String, dynamic> json) => _DriveTask(
|
||||||
|
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(_$DriveTaskStatusEnumMap, json['status']),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
type: json['type'] as String,
|
||||||
|
transmissionProgress: (json['transmission_progress'] as num?)?.toDouble(),
|
||||||
|
errorMessage: json['error_message'] as String?,
|
||||||
|
statusMessage: json['status_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> _$DriveTaskToJson(_DriveTask 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': _$DriveTaskStatusEnumMap[instance.status]!,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'type': instance.type,
|
||||||
|
'transmission_progress': instance.transmissionProgress,
|
||||||
|
'error_message': instance.errorMessage,
|
||||||
|
'status_message': instance.statusMessage,
|
||||||
|
'result': instance.result?.toJson(),
|
||||||
|
'pool_id': instance.poolId,
|
||||||
|
'bundle_id': instance.bundleId,
|
||||||
|
'encrypt_password': instance.encryptPassword,
|
||||||
|
'expired_at': instance.expiredAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$DriveTaskStatusEnumMap = {
|
||||||
|
DriveTaskStatus.pending: 'pending',
|
||||||
|
DriveTaskStatus.inProgress: 'inProgress',
|
||||||
|
DriveTaskStatus.paused: 'paused',
|
||||||
|
DriveTaskStatus.completed: 'completed',
|
||||||
|
DriveTaskStatus.failed: 'failed',
|
||||||
|
DriveTaskStatus.expired: 'expired',
|
||||||
|
DriveTaskStatus.cancelled: 'cancelled',
|
||||||
|
};
|
||||||
@@ -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";
|
||||||
@@ -28,7 +29,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
late final SnChatMember _identity;
|
late final SnChatMember _identity;
|
||||||
|
|
||||||
final Map<String, LocalChatMessage> _pendingMessages = {};
|
final Map<String, LocalChatMessage> _pendingMessages = {};
|
||||||
final Map<String, Map<int, double>> _fileUploadProgress = {};
|
final Map<String, Map<int, double?>> _fileUploadProgress = {};
|
||||||
int? _totalCount;
|
int? _totalCount;
|
||||||
String? _searchQuery;
|
String? _searchQuery;
|
||||||
bool? _withLinks;
|
bool? _withLinks;
|
||||||
@@ -433,12 +434,13 @@ 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,
|
||||||
SnChatMessage? forwardingTo,
|
SnChatMessage? forwardingTo,
|
||||||
SnChatMessage? replyingTo,
|
SnChatMessage? replyingTo,
|
||||||
Function(String, Map<int, double>)? onProgress,
|
Function(String, Map<int, double?>)? onProgress,
|
||||||
}) async {
|
}) async {
|
||||||
final nonce = const Uuid().v4();
|
final nonce = const Uuid().v4();
|
||||||
talker.log('Sending message with nonce $nonce');
|
talker.log('Sending message with nonce $nonce');
|
||||||
@@ -471,10 +473,10 @@ 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;
|
_fileUploadProgress[localMessage.id]?[idx] = progress ?? 0.0;
|
||||||
onProgress?.call(
|
onProgress?.call(
|
||||||
localMessage.id,
|
localMessage.id,
|
||||||
_fileUploadProgress[localMessage.id] ?? {},
|
_fileUploadProgress[localMessage.id] ?? {},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$messagesNotifierHash() => r'6adefd9152cdd686c2a863964993f24c42d405b5';
|
String _$messagesNotifierHash() => r'c009eb8598e8b5fbcece2d0b5213b2e434edb3b2';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|||||||
492
lib/pods/upload_tasks.dart
Normal file
492
lib/pods/upload_tasks.dart
Normal file
@@ -0,0 +1,492 @@
|
|||||||
|
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/drive_task.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/pods/websocket.dart';
|
||||||
|
import 'package:island/services/file_uploader.dart';
|
||||||
|
import 'package:island/talker.dart';
|
||||||
|
|
||||||
|
final uploadTasksProvider =
|
||||||
|
StateNotifierProvider<UploadTasksNotifier, List<DriveTask>>(
|
||||||
|
(ref) => UploadTasksNotifier(ref),
|
||||||
|
);
|
||||||
|
|
||||||
|
class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
||||||
|
final Ref ref;
|
||||||
|
StreamSubscription? _websocketSubscription;
|
||||||
|
final Map<String, Map<String, dynamic>> _pendingUploads = {};
|
||||||
|
|
||||||
|
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('task.')) {
|
||||||
|
final data = packet.data;
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
talker.info(
|
||||||
|
'[UploadTasks] Received WebSocket packet: ${packet.type}, data: $data',
|
||||||
|
);
|
||||||
|
|
||||||
|
final taskId = data['task_id'] as String?;
|
||||||
|
if (taskId == null) return;
|
||||||
|
|
||||||
|
switch (packet.type) {
|
||||||
|
case 'task.created':
|
||||||
|
_handleTaskCreated(taskId, data);
|
||||||
|
break;
|
||||||
|
case 'task.progress':
|
||||||
|
_handleProgressUpdate(taskId, data);
|
||||||
|
break;
|
||||||
|
case 'task.completed':
|
||||||
|
_handleUploadCompleted(taskId, data);
|
||||||
|
break;
|
||||||
|
case 'task.failed':
|
||||||
|
_handleUploadFailed(taskId, data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTaskCreated(String taskId, Map<String, dynamic> data) {
|
||||||
|
talker.info('[UploadTasks] Handling task.created for taskId: $taskId');
|
||||||
|
|
||||||
|
// 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.map((task) {
|
||||||
|
if (task.taskId == taskId) {
|
||||||
|
return task.copyWith(
|
||||||
|
status: DriveTaskStatus.pending,
|
||||||
|
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 = DriveTask(
|
||||||
|
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
taskId: taskId,
|
||||||
|
fileName: metadata['file_name'] as String,
|
||||||
|
contentType: metadata['mime_type'] as String,
|
||||||
|
fileSize: metadata['file_size'] as int,
|
||||||
|
uploadedBytes: 0,
|
||||||
|
totalChunks: metadata['total_chunks'] as int,
|
||||||
|
uploadedChunks: 0,
|
||||||
|
status: DriveTaskStatus.pending,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
type: 'FileUpload',
|
||||||
|
poolId: metadata['pool_id'] as String?,
|
||||||
|
bundleId: metadata['bundleId'] as String?,
|
||||||
|
encryptPassword: metadata['encrypt_password'] as String?,
|
||||||
|
expiredAt: metadata['expired_at'] 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 params = data['parameters'];
|
||||||
|
final uploadTask = DriveTask(
|
||||||
|
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
taskId: taskId,
|
||||||
|
fileName: params['file_name'] as String? ?? 'Unknown file',
|
||||||
|
contentType: params['content_type'],
|
||||||
|
fileSize: params['file_size'],
|
||||||
|
uploadedBytes:
|
||||||
|
(params['chunk_size'] as int) * (params['chunks_uploaded'] as int),
|
||||||
|
totalChunks: params['chunks_count'],
|
||||||
|
uploadedChunks: params['chunks_uploaded'],
|
||||||
|
status: DriveTaskStatus.pending,
|
||||||
|
createdAt: DateTime.tryParse(data['created_at']) ?? DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
type: data['type'],
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
statusMessage: data['status'],
|
||||||
|
uploadedBytes: uploadedBytes,
|
||||||
|
status: DriveTaskStatus.inProgress,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleUploadCompleted(String taskId, Map<String, dynamic> data) {
|
||||||
|
final results = data['results'] as Map<String, dynamic>?;
|
||||||
|
|
||||||
|
state =
|
||||||
|
state.map((task) {
|
||||||
|
if (task.taskId == taskId) {
|
||||||
|
return task.copyWith(
|
||||||
|
status: DriveTaskStatus.completed,
|
||||||
|
uploadedChunks: task.totalChunks,
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleUploadFailed(String taskId, Map<String, dynamic> data) {
|
||||||
|
final errorMessage = data['error_message'] as String? ?? 'Upload failed';
|
||||||
|
|
||||||
|
state =
|
||||||
|
state.map((task) {
|
||||||
|
if (task.taskId == taskId) {
|
||||||
|
return task.copyWith(
|
||||||
|
status: DriveTaskStatus.failed,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addUploadTask(DriveTask 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] = {
|
||||||
|
'file_name': fileName,
|
||||||
|
'mime_type': contentType,
|
||||||
|
'file_size': fileSize,
|
||||||
|
'total_chunks': totalChunks,
|
||||||
|
'pool_id': poolId,
|
||||||
|
'bundleId': bundleId,
|
||||||
|
'encrypt_password': encryptPassword,
|
||||||
|
'expired_at': expiredAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTaskStatus(
|
||||||
|
String taskId,
|
||||||
|
DriveTaskStatus 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 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) {
|
||||||
|
state = state.where((task) => task.taskId != taskId).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearCompletedTasks() {
|
||||||
|
state =
|
||||||
|
state
|
||||||
|
.where(
|
||||||
|
(task) =>
|
||||||
|
task.status != DriveTaskStatus.completed &&
|
||||||
|
task.status != DriveTaskStatus.failed &&
|
||||||
|
task.status != DriveTaskStatus.cancelled &&
|
||||||
|
task.status != DriveTaskStatus.expired,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
DriveTask? getTask(String taskId) {
|
||||||
|
return state.where((task) => task.taskId == taskId).firstOrNull;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DriveTask> getActiveTasks() {
|
||||||
|
return state
|
||||||
|
.where(
|
||||||
|
(task) =>
|
||||||
|
task.status == DriveTaskStatus.pending ||
|
||||||
|
task.status == DriveTaskStatus.inProgress ||
|
||||||
|
task.status == DriveTaskStatus.paused ||
|
||||||
|
task.status == DriveTaskStatus.completed,
|
||||||
|
)
|
||||||
|
.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,
|
||||||
|
);
|
||||||
|
|
||||||
|
int totalSize;
|
||||||
|
if (fileData is XFile) {
|
||||||
|
totalSize = await fileData.length();
|
||||||
|
} else if (fileData is Uint8List) {
|
||||||
|
totalSize = fileData.length;
|
||||||
|
} else {
|
||||||
|
throw ArgumentError('Invalid fileData type');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = DriveTask(
|
||||||
|
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
taskId: taskId,
|
||||||
|
fileName: fileName,
|
||||||
|
contentType: contentType,
|
||||||
|
fileSize: totalSize,
|
||||||
|
uploadedBytes: totalSize,
|
||||||
|
totalChunks: 1, // For existing files, we consider it as 1 chunk
|
||||||
|
uploadedChunks: 1,
|
||||||
|
status: DriveTaskStatus.completed,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
type: 'FileUpload',
|
||||||
|
poolId: poolId,
|
||||||
|
bundleId: bundleId,
|
||||||
|
encryptPassword: encryptPassword,
|
||||||
|
expiredAt: expiredAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
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
|
||||||
|
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);
|
||||||
|
// Update transmission progress in UI
|
||||||
|
ref
|
||||||
|
.read(uploadTasksProvider.notifier)
|
||||||
|
.updateTransmissionProgress(taskId, overallProgress);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
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);
|
||||||
|
// Update transmission progress in UI
|
||||||
|
ref
|
||||||
|
.read(uploadTasksProvider.notifier)
|
||||||
|
.updateTransmissionProgress(taskId, overallProgress);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
bytesUploaded += chunks[i].length;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw ArgumentError('Invalid fileData type');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Complete upload
|
||||||
|
onProgress?.call(null, Duration.zero);
|
||||||
|
return await completeUpload(taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,6 @@ import 'package:island/screens/stickers/pack_detail.dart';
|
|||||||
import 'package:island/screens/discovery/feeds/feed_marketplace.dart';
|
import 'package:island/screens/discovery/feeds/feed_marketplace.dart';
|
||||||
import 'package:island/screens/discovery/feeds/feed_detail.dart';
|
import 'package:island/screens/discovery/feeds/feed_detail.dart';
|
||||||
import 'package:island/screens/creators/poll/poll_list.dart';
|
import 'package:island/screens/creators/poll/poll_list.dart';
|
||||||
import 'package:island/screens/creators/publishers_form.dart';
|
|
||||||
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
|
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
|
||||||
import 'package:island/screens/poll/poll_editor.dart';
|
import 'package:island/screens/poll/poll_editor.dart';
|
||||||
import 'package:island/screens/posts/compose.dart';
|
import 'package:island/screens/posts/compose.dart';
|
||||||
@@ -507,19 +506,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return StickersScreen(pubName: name);
|
return StickersScreen(pubName: name);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
name: 'creatorNew',
|
|
||||||
path: 'new',
|
|
||||||
builder: (context, state) => const NewPublisherScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'creatorEdit',
|
|
||||||
path: ':name/edit',
|
|
||||||
builder: (context, state) {
|
|
||||||
final name = state.pathParameters['name']!;
|
|
||||||
return EditPublisherScreen(name: name);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class AccountScreen extends HookConsumerWidget {
|
|||||||
pathParameters: {'name': user.value!.name},
|
pathParameters: {'name': user.value!.name},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
).padding(bottom: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|||||||
@@ -84,9 +84,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
|
|||||||
'accountPasswordChange'.tr(),
|
'accountPasswordChange'.tr(),
|
||||||
);
|
);
|
||||||
if (!confirm || !context.mounted) return;
|
if (!confirm || !context.mounted) return;
|
||||||
final captchaTk = await Navigator.of(
|
final captchaTk = await CaptchaScreen.show(context);
|
||||||
context,
|
|
||||||
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
|
|
||||||
if (captchaTk == null) return;
|
if (captchaTk == null) return;
|
||||||
try {
|
try {
|
||||||
if (context.mounted) showLoadingModal(context);
|
if (context.mounted) showLoadingModal(context);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -2,9 +2,17 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:island/screens/auth/captcha.config.dart';
|
import 'package:island/screens/auth/captcha.config.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
|
||||||
class CaptchaScreen extends ConsumerWidget {
|
class CaptchaScreen extends ConsumerWidget {
|
||||||
|
static Future<String?> show(BuildContext context) {
|
||||||
|
return showModalBottomSheet<String>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) => const CaptchaScreen(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const CaptchaScreen({super.key});
|
const CaptchaScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -13,9 +21,9 @@ class CaptchaScreen extends ConsumerWidget {
|
|||||||
|
|
||||||
if (!captchaUrl.hasValue) return Center(child: CircularProgressIndicator());
|
if (!captchaUrl.hasValue) return Center(child: CircularProgressIndicator());
|
||||||
|
|
||||||
return AppScaffold(
|
return SheetScaffold(
|
||||||
appBar: AppBar(title: Text("Anti-Robot")),
|
titleText: "Anti-Robot",
|
||||||
body: InAppWebView(
|
child: InAppWebView(
|
||||||
initialUrlRequest: URLRequest(
|
initialUrlRequest: URLRequest(
|
||||||
url: WebUri('${captchaUrl.value}?redirect_uri=solian://captcha'),
|
url: WebUri('${captchaUrl.value}?redirect_uri=solian://captcha'),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,11 +4,19 @@ import 'dart:ui_web' as ui;
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/screens/auth/captcha.config.dart';
|
import 'package:island/screens/auth/captcha.config.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:web/web.dart' as web;
|
import 'package:web/web.dart' as web;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class CaptchaScreen extends ConsumerStatefulWidget {
|
class CaptchaScreen extends ConsumerStatefulWidget {
|
||||||
|
static Future<String?> show(BuildContext context) {
|
||||||
|
return showModalBottomSheet<String>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) => const CaptchaScreen(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const CaptchaScreen({super.key});
|
const CaptchaScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -61,9 +69,9 @@ class _CaptchaScreenState extends ConsumerState<CaptchaScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return SheetScaffold(
|
||||||
appBar: AppBar(title: Text("Anti-Robot")),
|
titleText: "Anti-Robot",
|
||||||
body:
|
child:
|
||||||
_isInitialized
|
_isInitialized
|
||||||
? HtmlElementView(viewType: 'captcha-iframe')
|
? HtmlElementView(viewType: 'captcha-iframe')
|
||||||
: Center(child: CircularProgressIndicator()),
|
: Center(child: CircularProgressIndicator()),
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ class CreateAccountScreen extends HookConsumerWidget {
|
|||||||
void performAction() async {
|
void performAction() async {
|
||||||
if (!formKey.currentState!.validate()) return;
|
if (!formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
final captchaTk = await Navigator.of(
|
final captchaTk = await CaptchaScreen.show(context);
|
||||||
context,
|
|
||||||
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
|
|
||||||
if (captchaTk == null) return;
|
if (captchaTk == null) return;
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|||||||
@@ -523,9 +523,7 @@ class _LoginLookupScreen extends HookConsumerWidget {
|
|||||||
showErrorAlert('loginResetPasswordHint'.tr());
|
showErrorAlert('loginResetPasswordHint'.tr());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final captchaTk = await Navigator.of(
|
final captchaTk = await CaptchaScreen.show(context);
|
||||||
context,
|
|
||||||
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
|
|
||||||
if (captchaTk == null) return;
|
if (captchaTk == null) return;
|
||||||
isBusy.value = true;
|
isBusy.value = true;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import "dart:async";
|
|||||||
import "dart:math" as math;
|
import "dart:math" as math;
|
||||||
import "package:easy_localization/easy_localization.dart";
|
import "package:easy_localization/easy_localization.dart";
|
||||||
import "package:file_picker/file_picker.dart";
|
import "package:file_picker/file_picker.dart";
|
||||||
|
import "package:image_picker/image_picker.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:go_router/go_router.dart";
|
import "package:go_router/go_router.dart";
|
||||||
import "package:flutter_hooks/flutter_hooks.dart";
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
@@ -148,7 +149,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
final messageForwardingTo = useState<SnChatMessage?>(null);
|
final messageForwardingTo = useState<SnChatMessage?>(null);
|
||||||
final messageEditingTo = useState<SnChatMessage?>(null);
|
final messageEditingTo = useState<SnChatMessage?>(null);
|
||||||
final attachments = useState<List<UniversalFile>>([]);
|
final attachments = useState<List<UniversalFile>>([]);
|
||||||
final attachmentProgress = useState<Map<String, Map<int, double>>>({});
|
final attachmentProgress = useState<Map<String, Map<int, double?>>>({});
|
||||||
|
|
||||||
// Selection mode state
|
// Selection mode state
|
||||||
final isSelectionMode = useState<bool>(false);
|
final isSelectionMode = useState<bool>(false);
|
||||||
@@ -181,16 +182,13 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
}, [scrollController]);
|
}, [scrollController]);
|
||||||
|
|
||||||
Future<void> pickPhotoMedia() async {
|
Future<void> pickPhotoMedia() async {
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final ImagePicker picker = ImagePicker();
|
||||||
type: FileType.image,
|
final List<XFile> results = await picker.pickMultiImage();
|
||||||
allowMultiple: true,
|
if (results.isEmpty) return;
|
||||||
allowCompression: false,
|
|
||||||
);
|
|
||||||
if (result == null || result.count == 0) return;
|
|
||||||
attachments.value = [
|
attachments.value = [
|
||||||
...attachments.value,
|
...attachments.value,
|
||||||
...result.files.map(
|
...results.map(
|
||||||
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.image),
|
(xfile) => UniversalFile(data: xfile, type: UniversalFileType.image),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -267,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,
|
||||||
@@ -563,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:
|
||||||
@@ -573,7 +572,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
onProgress: (progress, _) {
|
onProgress: (progress, _) {
|
||||||
attachmentProgress.value = {
|
attachmentProgress.value = {
|
||||||
...attachmentProgress.value,
|
...attachmentProgress.value,
|
||||||
'chat-upload': {index: progress},
|
'chat-upload': {index: progress ?? 0.0},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
).future;
|
).future;
|
||||||
|
|||||||
@@ -261,7 +261,11 @@ class _PublisherUnselectedWidget extends HookConsumerWidget {
|
|||||||
subtitle: Text('createPublisherHint').tr(),
|
subtitle: Text('createPublisherHint').tr(),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushNamed('creatorNew').then((value) {
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) => const NewPublisherScreen(),
|
||||||
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
ref.invalidate(publishersManagedProvider);
|
ref.invalidate(publishersManagedProvider);
|
||||||
}
|
}
|
||||||
@@ -285,19 +289,18 @@ class CreatorHubScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
void updatePublisher() {
|
void updatePublisher() {
|
||||||
context
|
showModalBottomSheet(
|
||||||
.pushNamed(
|
context: context,
|
||||||
'creatorEdit',
|
isScrollControlled: true,
|
||||||
pathParameters: {'name': currentPublisher.value!.name},
|
builder:
|
||||||
)
|
(context) =>
|
||||||
.then((value) async {
|
EditPublisherScreen(name: currentPublisher.value!.name),
|
||||||
if (value == null) return;
|
).then((value) async {
|
||||||
final data = await ref.refresh(publishersManagedProvider.future);
|
if (value == null) return;
|
||||||
currentPublisher.value =
|
final data = await ref.refresh(publishersManagedProvider.future);
|
||||||
data
|
currentPublisher.value =
|
||||||
.where((e) => e.id == currentPublisher.value!.id)
|
data.where((e) => e.id == currentPublisher.value!.id).firstOrNull;
|
||||||
.firstOrNull;
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void deletePublisher() {
|
void deletePublisher() {
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import 'package:island/screens/realm/realms.dart';
|
|||||||
import 'package:island/services/file.dart';
|
import 'package:island/services/file.dart';
|
||||||
import 'package:island/services/file_uploader.dart';
|
import 'package:island/services/file_uploader.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
@@ -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...');
|
||||||
@@ -177,13 +177,11 @@ class EditPublisherScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
final titleText = (name == null ? 'createPublisher' : 'editPublisher').tr();
|
||||||
isNoBackground: false,
|
|
||||||
appBar: AppBar(
|
return SheetScaffold(
|
||||||
title: Text(name == null ? 'createPublisher' : 'editPublisher').tr(),
|
titleText: titleText,
|
||||||
leading: const PageBackButton(),
|
child: SingleChildScrollView(
|
||||||
),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
padding: EdgeInsets.only(bottom: 16),
|
padding: EdgeInsets.only(bottom: 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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...');
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
ValueListenableBuilder<Map<int, double>>(
|
ValueListenableBuilder<Map<int, double?>>(
|
||||||
valueListenable: state.attachmentProgress,
|
valueListenable: state.attachmentProgress,
|
||||||
builder: (context, progressMap, _) {
|
builder: (context, progressMap, _) {
|
||||||
return Wrap(
|
return Wrap(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'package:convert/convert.dart';
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:cross_file/cross_file.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@@ -6,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;
|
||||||
@@ -21,9 +23,51 @@ class FileUploader {
|
|||||||
return digest.toString();
|
return digest.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculates the MD5 hash from a stream.
|
||||||
|
Future<String> _calculateFileHashFromStream(Stream<List<int>> stream) async {
|
||||||
|
final accumulator = AccumulatorSink<Digest>();
|
||||||
|
final converter = md5.startChunkedConversion(accumulator);
|
||||||
|
await for (final chunk in stream) {
|
||||||
|
converter.add(chunk);
|
||||||
|
}
|
||||||
|
converter.close();
|
||||||
|
final digest = accumulator.events.single;
|
||||||
|
return digest.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the next chunk from a stream subscription.
|
||||||
|
Future<Uint8List> _readNextChunk(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates an upload task for the given file.
|
/// Creates an upload task for the given file.
|
||||||
Future<Map<String, dynamic>> createUploadTask({
|
Future<Map<String, dynamic>> createUploadTask({
|
||||||
required Uint8List bytes,
|
required dynamic fileData,
|
||||||
required String fileName,
|
required String fileName,
|
||||||
required String contentType,
|
required String contentType,
|
||||||
String? poolId,
|
String? poolId,
|
||||||
@@ -32,8 +76,17 @@ class FileUploader {
|
|||||||
String? expiredAt,
|
String? expiredAt,
|
||||||
int? chunkSize,
|
int? chunkSize,
|
||||||
}) async {
|
}) async {
|
||||||
final hash = _calculateFileHash(bytes);
|
String hash;
|
||||||
final fileSize = bytes.length;
|
int fileSize;
|
||||||
|
if (fileData is XFile) {
|
||||||
|
fileSize = await fileData.length();
|
||||||
|
hash = await _calculateFileHashFromStream(fileData.openRead());
|
||||||
|
} else if (fileData is Uint8List) {
|
||||||
|
hash = _calculateFileHash(fileData);
|
||||||
|
fileSize = fileData.length;
|
||||||
|
} else {
|
||||||
|
throw ArgumentError('Invalid fileData type');
|
||||||
|
}
|
||||||
|
|
||||||
final response = await _client.post(
|
final response = await _client.post(
|
||||||
'/drive/files/upload/create',
|
'/drive/files/upload/create',
|
||||||
@@ -58,6 +111,7 @@ class FileUploader {
|
|||||||
required String taskId,
|
required String taskId,
|
||||||
required int chunkIndex,
|
required int chunkIndex,
|
||||||
required Uint8List chunkData,
|
required Uint8List chunkData,
|
||||||
|
ProgressCallback? onSendProgress,
|
||||||
}) async {
|
}) async {
|
||||||
final formData = FormData.fromMap({
|
final formData = FormData.fromMap({
|
||||||
'chunk': MultipartFile.fromBytes(
|
'chunk': MultipartFile.fromBytes(
|
||||||
@@ -69,19 +123,26 @@ class FileUploader {
|
|||||||
await _client.post(
|
await _client.post(
|
||||||
'/drive/files/upload/chunk/$taskId/$chunkIndex',
|
'/drive/files/upload/chunk/$taskId/$chunkIndex',
|
||||||
data: formData,
|
data: formData,
|
||||||
|
onSendProgress: onSendProgress,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Completes the upload and returns the CloudFile object.
|
/// Completes the upload and returns the CloudFile object.
|
||||||
Future<SnCloudFile> completeUpload(String taskId) async {
|
Future<SnCloudFile> completeUpload(String taskId) async {
|
||||||
final response = await _client.post('/drive/files/upload/complete/$taskId');
|
final response = await _client.post(
|
||||||
|
'/drive/files/upload/complete/$taskId',
|
||||||
|
options: Options(
|
||||||
|
sendTimeout: Duration(minutes: 1),
|
||||||
|
receiveTimeout: Duration(minutes: 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return SnCloudFile.fromJson(response.data);
|
return SnCloudFile.fromJson(response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Uploads a file in chunks using the multi-part API.
|
/// Uploads a file in chunks using the multi-part API.
|
||||||
Future<SnCloudFile> uploadFile({
|
Future<SnCloudFile> uploadFile({
|
||||||
required Uint8List bytes,
|
required dynamic fileData,
|
||||||
required String fileName,
|
required String fileName,
|
||||||
required String contentType,
|
required String contentType,
|
||||||
String? poolId,
|
String? poolId,
|
||||||
@@ -89,10 +150,12 @@ class FileUploader {
|
|||||||
String? encryptPassword,
|
String? encryptPassword,
|
||||||
String? expiredAt,
|
String? expiredAt,
|
||||||
int? customChunkSize,
|
int? customChunkSize,
|
||||||
|
Function(double? progress, Duration estimate)? onProgress,
|
||||||
}) async {
|
}) async {
|
||||||
// Step 1: Create upload task
|
// Step 1: Create upload task
|
||||||
|
onProgress?.call(null, Duration.zero);
|
||||||
final createResponse = await createUploadTask(
|
final createResponse = await createUploadTask(
|
||||||
bytes: bytes,
|
fileData: fileData,
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
contentType: contentType,
|
contentType: contentType,
|
||||||
poolId: poolId,
|
poolId: poolId,
|
||||||
@@ -110,36 +173,73 @@ class FileUploader {
|
|||||||
final taskId = createResponse['task_id'] as String;
|
final taskId = createResponse['task_id'] as String;
|
||||||
final chunkSize = createResponse['chunk_size'] as int;
|
final chunkSize = createResponse['chunk_size'] as int;
|
||||||
final chunksCount = createResponse['chunks_count'] 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');
|
||||||
|
}
|
||||||
|
|
||||||
// Step 2: Upload chunks
|
// Step 2: Upload chunks
|
||||||
final chunks = <Uint8List>[];
|
int bytesUploaded = 0;
|
||||||
for (int i = 0; i < bytes.length; i += chunkSize) {
|
if (fileData is XFile) {
|
||||||
final end = i + chunkSize > bytes.length ? bytes.length : i + chunkSize;
|
// Use stream for XFile
|
||||||
chunks.add(Uint8List.fromList(bytes.sublist(i, end)));
|
final subscription = fileData.openRead().listen(null);
|
||||||
}
|
subscription.pause();
|
||||||
|
for (int i = 0; i < chunksCount; i++) {
|
||||||
|
subscription.resume();
|
||||||
|
final chunkData = await _readNextChunk(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)));
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure we have the correct number of chunks
|
// Upload each chunk
|
||||||
if (chunks.length != chunksCount) {
|
for (int i = 0; i < chunks.length; i++) {
|
||||||
throw Exception(
|
await uploadChunk(
|
||||||
'Chunk count mismatch: expected $chunksCount, got ${chunks.length}',
|
taskId: taskId,
|
||||||
);
|
chunkIndex: i,
|
||||||
}
|
chunkData: chunks[i],
|
||||||
|
onSendProgress: (sent, total) {
|
||||||
// Upload each chunk
|
final overallProgress = (bytesUploaded + sent) / totalSize;
|
||||||
for (int i = 0; i < chunks.length; i++) {
|
onProgress?.call(overallProgress, Duration.zero);
|
||||||
await uploadChunk(taskId: taskId, chunkIndex: i, chunkData: chunks[i]);
|
},
|
||||||
|
);
|
||||||
|
bytesUploaded += chunks[i].length;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw ArgumentError('Invalid fileData type');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Complete upload
|
// Step 3: Complete upload
|
||||||
|
onProgress?.call(null, Duration.zero);
|
||||||
return await completeUpload(taskId);
|
return await completeUpload(taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
}) {
|
}) {
|
||||||
final completer = Completer<SnCloudFile?>();
|
final completer = Completer<SnCloudFile?>();
|
||||||
|
|
||||||
@@ -173,19 +273,14 @@ class FileUploader {
|
|||||||
await exif.writeAttributes(gpsAttributes);
|
await exif.writeAttributes(gpsAttributes);
|
||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
(_) => _processUpload(
|
(_) =>
|
||||||
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 _processUpload(
|
return _processUpload(
|
||||||
fileData,
|
fileData,
|
||||||
client,
|
ref,
|
||||||
poolId,
|
poolId,
|
||||||
onProgress,
|
onProgress,
|
||||||
completer,
|
completer,
|
||||||
@@ -196,16 +291,16 @@ class FileUploader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_processUpload(fileData, client, poolId, onProgress, completer);
|
_processUpload(fileData, ref, 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?> _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,
|
||||||
) {
|
) {
|
||||||
String actualMimetype = getMimeType(fileData);
|
String actualMimetype = getMimeType(fileData);
|
||||||
@@ -216,23 +311,15 @@ class FileUploader {
|
|||||||
final data = fileData.data;
|
final data = fileData.data;
|
||||||
|
|
||||||
if (data is XFile) {
|
if (data is XFile) {
|
||||||
// Read bytes from XFile
|
_performUpload(
|
||||||
data
|
fileData: data,
|
||||||
.readAsBytes()
|
fileName: fileData.displayName ?? data.name,
|
||||||
.then((readBytes) {
|
contentType: actualMimetype,
|
||||||
_performUpload(
|
ref: ref,
|
||||||
bytes: readBytes,
|
poolId: poolId,
|
||||||
fileName: fileData.displayName ?? data.name,
|
onProgress: onProgress,
|
||||||
contentType: actualMimetype,
|
completer: completer,
|
||||||
client: client,
|
);
|
||||||
poolId: poolId,
|
|
||||||
onProgress: onProgress,
|
|
||||||
completer: completer,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catchError((e) {
|
|
||||||
completer.completeError(e);
|
|
||||||
});
|
|
||||||
return completer;
|
return completer;
|
||||||
} else if (data is List<int> || data is Uint8List) {
|
} else if (data is List<int> || data is Uint8List) {
|
||||||
bytes = data is List<int> ? Uint8List.fromList(data) : data;
|
bytes = data is List<int> ? Uint8List.fromList(data) : data;
|
||||||
@@ -252,10 +339,10 @@ class FileUploader {
|
|||||||
|
|
||||||
if (bytes != null) {
|
if (bytes != null) {
|
||||||
_performUpload(
|
_performUpload(
|
||||||
bytes: 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,
|
||||||
@@ -265,30 +352,32 @@ 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 Uint8List bytes,
|
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(0.0, Duration.zero);
|
onProgress?.call(null, Duration.zero);
|
||||||
uploader
|
uploader
|
||||||
.uploadFile(
|
.uploadFile(
|
||||||
bytes: bytes,
|
fileData: fileData,
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
contentType: contentType,
|
contentType: contentType,
|
||||||
poolId: poolId,
|
poolId: poolId,
|
||||||
|
onProgress: onProgress,
|
||||||
)
|
)
|
||||||
.then((result) {
|
.then((result) {
|
||||||
// Call progress end
|
// Call progress end
|
||||||
onProgress?.call(1.0, Duration.zero);
|
onProgress?.call(null, Duration.zero);
|
||||||
completer.complete(result);
|
completer.complete(result);
|
||||||
})
|
})
|
||||||
.catchError((e) {
|
.catchError((e) {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
final Function(int) onDeleteAttachment;
|
final Function(int) onDeleteAttachment;
|
||||||
final Function(int, int) onMoveAttachment;
|
final Function(int, int) onMoveAttachment;
|
||||||
final Function(List<UniversalFile>) onAttachmentsChanged;
|
final Function(List<UniversalFile>) onAttachmentsChanged;
|
||||||
final Map<String, Map<int, double>> attachmentProgress;
|
final Map<String, Map<int, double?>> attachmentProgress;
|
||||||
|
|
||||||
const ChatInput({
|
const ChatInput({
|
||||||
super.key,
|
super.key,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class MessageItem extends HookConsumerWidget {
|
|||||||
final LocalChatMessage message;
|
final LocalChatMessage message;
|
||||||
final bool isCurrentUser;
|
final bool isCurrentUser;
|
||||||
final Function(String action)? onAction;
|
final Function(String action)? onAction;
|
||||||
final Map<int, double>? progress;
|
final Map<int, double?>? progress;
|
||||||
final bool showAvatar;
|
final bool showAvatar;
|
||||||
final Function(String messageId) onJump;
|
final Function(String messageId) onJump;
|
||||||
final bool isSelectionMode;
|
final bool isSelectionMode;
|
||||||
@@ -689,7 +689,7 @@ class MessageHoverActionMenu extends StatelessWidget {
|
|||||||
class MessageItemDisplayBubble extends HookConsumerWidget {
|
class MessageItemDisplayBubble extends HookConsumerWidget {
|
||||||
final LocalChatMessage message;
|
final LocalChatMessage message;
|
||||||
final bool isCurrentUser;
|
final bool isCurrentUser;
|
||||||
final Map<int, double>? progress;
|
final Map<int, double?>? progress;
|
||||||
final bool showAvatar;
|
final bool showAvatar;
|
||||||
final Function(String messageId) onJump;
|
final Function(String messageId) onJump;
|
||||||
final String? translatedText;
|
final String? translatedText;
|
||||||
@@ -821,7 +821,7 @@ class MessageItemDisplayBubble extends HookConsumerWidget {
|
|||||||
class MessageItemDisplayIRC extends HookConsumerWidget {
|
class MessageItemDisplayIRC extends HookConsumerWidget {
|
||||||
final LocalChatMessage message;
|
final LocalChatMessage message;
|
||||||
final bool isCurrentUser;
|
final bool isCurrentUser;
|
||||||
final Map<int, double>? progress;
|
final Map<int, double?>? progress;
|
||||||
final bool showAvatar;
|
final bool showAvatar;
|
||||||
final Function(String messageId) onJump;
|
final Function(String messageId) onJump;
|
||||||
final String? translatedText;
|
final String? translatedText;
|
||||||
@@ -949,7 +949,7 @@ class MessageItemDisplayIRC extends HookConsumerWidget {
|
|||||||
class MessageItemDisplayDiscord extends HookConsumerWidget {
|
class MessageItemDisplayDiscord extends HookConsumerWidget {
|
||||||
final LocalChatMessage message;
|
final LocalChatMessage message;
|
||||||
final bool isCurrentUser;
|
final bool isCurrentUser;
|
||||||
final Map<int, double>? progress;
|
final Map<int, double?>? progress;
|
||||||
final bool showAvatar;
|
final bool showAvatar;
|
||||||
final Function(String messageId) onJump;
|
final Function(String messageId) onJump;
|
||||||
final String? translatedText;
|
final String? translatedText;
|
||||||
@@ -1238,7 +1238,7 @@ class MessageQuoteWidget extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FileUploadProgressWidget extends StatelessWidget {
|
class FileUploadProgressWidget extends StatelessWidget {
|
||||||
final Map<int, double>? progress;
|
final Map<int, double?>? progress;
|
||||||
final Color textColor;
|
final Color textColor;
|
||||||
final bool hasContent;
|
final bool hasContent;
|
||||||
|
|
||||||
@@ -1266,7 +1266,9 @@ class FileUploadProgressWidget extends StatelessWidget {
|
|||||||
'fileUploadingProgress'.tr(
|
'fileUploadingProgress'.tr(
|
||||||
args: [
|
args: [
|
||||||
(entry.key + 1).toString(),
|
(entry.key + 1).toString(),
|
||||||
(entry.value * 100).toStringAsFixed(1),
|
entry.value != null
|
||||||
|
? (entry.value! * 100).toStringAsFixed(1)
|
||||||
|
: '0.0',
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|||||||
@@ -104,9 +104,7 @@ class CheckInWidget extends HookConsumerWidget {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err is DioException) {
|
if (err is DioException) {
|
||||||
if (err.response?.statusCode == 423 && context.mounted) {
|
if (err.response?.statusCode == 423 && context.mounted) {
|
||||||
final captchaTk = await Navigator.of(
|
final captchaTk = await CaptchaScreen.show(context);
|
||||||
context,
|
|
||||||
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
|
|
||||||
if (captchaTk == null) return;
|
if (captchaTk == null) return;
|
||||||
return await checkIn(captchatTk: captchaTk);
|
return await checkIn(captchatTk: captchaTk);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -411,10 +411,7 @@ class AttachmentPreview extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Gap(6),
|
Gap(6),
|
||||||
Center(
|
Center(
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(value: progress),
|
||||||
value:
|
|
||||||
progress != null ? progress! / 100.0 : null,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
@@ -112,23 +111,28 @@ class CloudFilePicker extends HookConsumerWidget {
|
|||||||
|
|
||||||
void pickImage() async {
|
void pickImage() async {
|
||||||
showLoadingModal(context);
|
showLoadingModal(context);
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final ImagePicker picker = ImagePicker();
|
||||||
allowMultiple: allowMultiple,
|
List<XFile> results;
|
||||||
type: FileType.image,
|
if (allowMultiple) {
|
||||||
);
|
results = await picker.pickMultiImage();
|
||||||
if (result == null || result.files.isEmpty) {
|
} else {
|
||||||
|
final XFile? result = await picker.pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
);
|
||||||
|
results = result != null ? [result] : [];
|
||||||
|
}
|
||||||
|
if (results.isEmpty) {
|
||||||
if (context.mounted) hideLoadingModal(context);
|
if (context.mounted) hideLoadingModal(context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final newFiles =
|
final newFiles =
|
||||||
result.files.map((e) {
|
results
|
||||||
final xfile =
|
.map(
|
||||||
e.bytes != null
|
(xfile) =>
|
||||||
? XFile.fromData(e.bytes!, name: e.name)
|
UniversalFile(data: xfile, type: UniversalFileType.image),
|
||||||
: XFile(e.path!);
|
)
|
||||||
return UniversalFile(data: xfile, type: UniversalFileType.image);
|
.toList();
|
||||||
}).toList();
|
|
||||||
|
|
||||||
if (!allowMultiple) {
|
if (!allowMultiple) {
|
||||||
files.value = newFiles;
|
files.value = newFiles;
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class ArticleComposeAttachments extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
ValueListenableBuilder<Map<int, double>>(
|
ValueListenableBuilder<Map<int, double?>>(
|
||||||
valueListenable: state.attachmentProgress,
|
valueListenable: state.attachmentProgress,
|
||||||
builder: (context, progressMap, _) {
|
builder: (context, progressMap, _) {
|
||||||
return Wrap(
|
return Wrap(
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/models/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
@@ -330,7 +329,13 @@ class PostComposeCard extends HookConsumerWidget {
|
|||||||
if (isContained) {
|
if (isContained) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
context.pushNamed('creatorNew').then((value) {
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
useRootNavigator: true,
|
||||||
|
builder:
|
||||||
|
(context) => const NewPublisherScreen(),
|
||||||
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
composeState.currentPublisher.value =
|
composeState.currentPublisher.value =
|
||||||
value as SnPublisher;
|
value as SnPublisher;
|
||||||
@@ -368,9 +373,14 @@ class PostComposeCard extends HookConsumerWidget {
|
|||||||
if (isContained) {
|
if (isContained) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
context.pushNamed('creatorNew').then((
|
showModalBottomSheet(
|
||||||
value,
|
context: context,
|
||||||
) {
|
isScrollControlled: true,
|
||||||
|
useRootNavigator: true,
|
||||||
|
builder:
|
||||||
|
(context) =>
|
||||||
|
const NewPublisherScreen(),
|
||||||
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
composeState.currentPublisher.value =
|
composeState.currentPublisher.value =
|
||||||
value as SnPublisher;
|
value as SnPublisher;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class ComposeState {
|
|||||||
final TextEditingController slugController;
|
final TextEditingController slugController;
|
||||||
final ValueNotifier<int> visibility;
|
final ValueNotifier<int> visibility;
|
||||||
final ValueNotifier<List<UniversalFile>> attachments;
|
final ValueNotifier<List<UniversalFile>> attachments;
|
||||||
final ValueNotifier<Map<int, double>> attachmentProgress;
|
final ValueNotifier<Map<int, double?>> attachmentProgress;
|
||||||
final ValueNotifier<SnPublisher?> currentPublisher;
|
final ValueNotifier<SnPublisher?> currentPublisher;
|
||||||
final ValueNotifier<bool> submitting;
|
final ValueNotifier<bool> submitting;
|
||||||
final ValueNotifier<List<SnPostCategory>> categories;
|
final ValueNotifier<List<SnPostCategory>> categories;
|
||||||
@@ -123,7 +123,7 @@ class ComposeLogic {
|
|||||||
slugController: TextEditingController(text: originalPost?.slug),
|
slugController: TextEditingController(text: originalPost?.slug),
|
||||||
visibility: ValueNotifier<int>(originalPost?.visibility ?? 0),
|
visibility: ValueNotifier<int>(originalPost?.visibility ?? 0),
|
||||||
submitting: ValueNotifier<bool>(false),
|
submitting: ValueNotifier<bool>(false),
|
||||||
attachmentProgress: ValueNotifier<Map<int, double>>({}),
|
attachmentProgress: ValueNotifier<Map<int, double?>>({}),
|
||||||
currentPublisher: ValueNotifier<SnPublisher?>(originalPost?.publisher),
|
currentPublisher: ValueNotifier<SnPublisher?>(originalPost?.publisher),
|
||||||
tags: ValueNotifier<List<String>>(tags),
|
tags: ValueNotifier<List<String>>(tags),
|
||||||
categories: ValueNotifier<List<SnPostCategory>>(categories),
|
categories: ValueNotifier<List<SnPostCategory>>(categories),
|
||||||
@@ -149,7 +149,7 @@ class ComposeLogic {
|
|||||||
slugController: TextEditingController(text: draft.slug),
|
slugController: TextEditingController(text: draft.slug),
|
||||||
visibility: ValueNotifier<int>(draft.visibility),
|
visibility: ValueNotifier<int>(draft.visibility),
|
||||||
submitting: ValueNotifier<bool>(false),
|
submitting: ValueNotifier<bool>(false),
|
||||||
attachmentProgress: ValueNotifier<Map<int, double>>({}),
|
attachmentProgress: ValueNotifier<Map<int, double?>>({}),
|
||||||
currentPublisher: ValueNotifier<SnPublisher?>(null),
|
currentPublisher: ValueNotifier<SnPublisher?>(null),
|
||||||
tags: ValueNotifier<List<String>>(tags),
|
tags: ValueNotifier<List<String>>(tags),
|
||||||
categories: ValueNotifier<List<SnPostCategory>>(draft.categories),
|
categories: ValueNotifier<List<SnPostCategory>>(draft.categories),
|
||||||
@@ -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) {
|
||||||
@@ -402,16 +402,13 @@ class ComposeLogic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> pickPhotoMedia(WidgetRef ref, ComposeState state) async {
|
static Future<void> pickPhotoMedia(WidgetRef ref, ComposeState state) async {
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final ImagePicker picker = ImagePicker();
|
||||||
type: FileType.image,
|
final List<XFile> results = await picker.pickMultiImage();
|
||||||
allowMultiple: true,
|
if (results.isEmpty) return;
|
||||||
allowCompression: false,
|
|
||||||
);
|
|
||||||
if (result == null || result.count == 0) return;
|
|
||||||
state.attachments.value = [
|
state.attachments.value = [
|
||||||
...state.attachments.value,
|
...state.attachments.value,
|
||||||
...result.files.map(
|
...results.map(
|
||||||
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.image),
|
(xfile) => UniversalFile(data: xfile, type: UniversalFileType.image),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -503,7 +500,7 @@ class ComposeLogic {
|
|||||||
try {
|
try {
|
||||||
state.attachmentProgress.value = {
|
state.attachmentProgress.value = {
|
||||||
...state.attachmentProgress.value,
|
...state.attachmentProgress.value,
|
||||||
index: 0,
|
index: 0.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
SnCloudFile? cloudFile;
|
SnCloudFile? cloudFile;
|
||||||
@@ -513,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:
|
||||||
@@ -523,7 +520,7 @@ class ComposeLogic {
|
|||||||
onProgress: (progress, _) {
|
onProgress: (progress, _) {
|
||||||
state.attachmentProgress.value = {
|
state.attachmentProgress.value = {
|
||||||
...state.attachmentProgress.value,
|
...state.attachmentProgress.value,
|
||||||
index: progress,
|
index: progress ?? 0.0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
).future;
|
).future;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'dart:math' as math;
|
|||||||
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:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/screens/creators/publishers_form.dart';
|
import 'package:island/screens/creators/publishers_form.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
@@ -43,9 +42,13 @@ class PublisherModal extends HookConsumerWidget {
|
|||||||
const Gap(12),
|
const Gap(12),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pushNamed('creatorNew').then((
|
showModalBottomSheet(
|
||||||
value,
|
context: context,
|
||||||
) {
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) =>
|
||||||
|
const NewPublisherScreen(),
|
||||||
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
ref.invalidate(
|
ref.invalidate(
|
||||||
publishersManagedProvider,
|
publishersManagedProvider,
|
||||||
|
|||||||
@@ -241,12 +241,13 @@ 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) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_fileUploadProgress[messageId]?[idx] = progress;
|
_fileUploadProgress[messageId]?[idx] =
|
||||||
|
progress ?? 0.0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -306,7 +307,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
|
|||||||
|
|
||||||
// Navigate to chat if requested
|
// Navigate to chat if requested
|
||||||
if (shouldNavigate == true && mounted) {
|
if (shouldNavigate == true && mounted) {
|
||||||
context.push('/sphere/chat/${chatRoom.id}');
|
context.push('/chat/${chatRoom.id}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
754
lib/widgets/upload_overlay.dart
Normal file
754
lib/widgets/upload_overlay.dart
Normal file
@@ -0,0 +1,754 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/drive_task.dart';
|
||||||
|
import 'package:island/pods/upload_tasks.dart';
|
||||||
|
import 'package:island/services/responsive.dart';
|
||||||
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.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 == DriveTaskStatus.pending ||
|
||||||
|
task.status == DriveTaskStatus.inProgress ||
|
||||||
|
task.status == DriveTaskStatus.paused ||
|
||||||
|
task.status == DriveTaskStatus.completed,
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
..sort((a, b) => b.createdAt.compareTo(a.createdAt)); // Newest first
|
||||||
|
|
||||||
|
final isVisible = activeTasks.isNotEmpty;
|
||||||
|
final slideController = useAnimationController(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
);
|
||||||
|
final slideAnimation = Tween<Offset>(
|
||||||
|
begin: const Offset(0, 1), // Start from below the screen
|
||||||
|
end: Offset.zero, // End at normal position
|
||||||
|
).animate(CurvedAnimation(parent: slideController, curve: Curves.easeOut));
|
||||||
|
|
||||||
|
// Animate when visibility changes
|
||||||
|
useEffect(() {
|
||||||
|
if (isVisible) {
|
||||||
|
slideController.forward();
|
||||||
|
} else {
|
||||||
|
slideController.reverse();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [isVisible]);
|
||||||
|
|
||||||
|
if (!isVisible && slideController.status == AnimationStatus.dismissed) {
|
||||||
|
// If not visible and animation is complete (back to start), don't show anything
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final isDesktop = isWideScreen(context);
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: isDesktop ? null : 0,
|
||||||
|
right: isDesktop ? 24 : 0,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: slideAnimation,
|
||||||
|
child: _UploadOverlayContent(
|
||||||
|
activeTasks: activeTasks,
|
||||||
|
).padding(bottom: 16 + MediaQuery.of(context).padding.bottom),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UploadOverlayContent extends HookConsumerWidget {
|
||||||
|
final List<DriveTask> 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 Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: isMobile ? 16 : 24,
|
||||||
|
left: isMobile ? 16 : 0,
|
||||||
|
right: isMobile ? 16 : 24,
|
||||||
|
),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => isExpanded.value = !isExpanded.value,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: animationController,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Material(
|
||||||
|
elevation: 8 + (opacityAnimation * 4),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
width: isMobile ? MediaQuery.of(context).size.width - 32 : 320,
|
||||||
|
height: heightAnimation,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Collapsed Header
|
||||||
|
Container(
|
||||||
|
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.list_rounded
|
||||||
|
: _getOverallStatusIcon(activeTasks),
|
||||||
|
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()
|
||||||
|
: _getOverallStatusText(activeTasks),
|
||||||
|
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).colorScheme.outline,
|
||||||
|
width:
|
||||||
|
1 /
|
||||||
|
MediaQuery.of(context).devicePixelRatio,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
// Clear completed tasks button
|
||||||
|
if (_hasCompletedTasks(activeTasks))
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: const Text('Clear Completed'),
|
||||||
|
leading: Icon(
|
||||||
|
Symbols.clear_all,
|
||||||
|
size: 18,
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
ref
|
||||||
|
.read(uploadTasksProvider.notifier)
|
||||||
|
.clearCompletedTasks();
|
||||||
|
},
|
||||||
|
tileColor:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainerHighest,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Task list
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate((
|
||||||
|
context,
|
||||||
|
index,
|
||||||
|
) {
|
||||||
|
final task = activeTasks[index];
|
||||||
|
return AnimatedOpacity(
|
||||||
|
opacity: opacityAnimation,
|
||||||
|
duration: const Duration(
|
||||||
|
milliseconds: 150,
|
||||||
|
),
|
||||||
|
child: UploadTaskTile(task: task),
|
||||||
|
);
|
||||||
|
}, childCount: activeTasks.length),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getOverallProgress(List<DriveTask> tasks) {
|
||||||
|
if (tasks.isEmpty) return 0.0;
|
||||||
|
final totalProgress = tasks.fold<double>(
|
||||||
|
0.0,
|
||||||
|
(sum, task) =>
|
||||||
|
sum +
|
||||||
|
(task.status == DriveTaskStatus.inProgress
|
||||||
|
? task.progress
|
||||||
|
: task.status == DriveTaskStatus.completed
|
||||||
|
? 1
|
||||||
|
: 0),
|
||||||
|
);
|
||||||
|
return totalProgress / tasks.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getOverallProgressText(List<DriveTask> tasks) {
|
||||||
|
final overallProgress = _getOverallProgress(tasks);
|
||||||
|
return '${(overallProgress * 100).toStringAsFixed(0)}%';
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getOverallStatusIcon(List<DriveTask> tasks) {
|
||||||
|
if (tasks.isEmpty) return Symbols.upload;
|
||||||
|
|
||||||
|
final hasInProgress = tasks.any(
|
||||||
|
(task) => task.status == DriveTaskStatus.inProgress,
|
||||||
|
);
|
||||||
|
final hasPending = tasks.any(
|
||||||
|
(task) => task.status == DriveTaskStatus.pending,
|
||||||
|
);
|
||||||
|
final hasPaused = tasks.any(
|
||||||
|
(task) => task.status == DriveTaskStatus.paused,
|
||||||
|
);
|
||||||
|
final hasFailed = tasks.any(
|
||||||
|
(task) =>
|
||||||
|
task.status == DriveTaskStatus.failed ||
|
||||||
|
task.status == DriveTaskStatus.cancelled ||
|
||||||
|
task.status == DriveTaskStatus.expired,
|
||||||
|
);
|
||||||
|
final hasCompleted = tasks.any(
|
||||||
|
(task) => task.status == DriveTaskStatus.completed,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Priority order: in progress > pending > paused > failed > completed
|
||||||
|
if (hasInProgress) {
|
||||||
|
return Symbols.upload;
|
||||||
|
} else if (hasPending) {
|
||||||
|
return Symbols.schedule;
|
||||||
|
} else if (hasPaused) {
|
||||||
|
return Symbols.pause_circle;
|
||||||
|
} else if (hasFailed) {
|
||||||
|
return Symbols.error;
|
||||||
|
} else if (hasCompleted) {
|
||||||
|
return Symbols.check_circle;
|
||||||
|
} else {
|
||||||
|
return Symbols.upload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getOverallStatusText(List<DriveTask> tasks) {
|
||||||
|
if (tasks.isEmpty) return '0 tasks';
|
||||||
|
|
||||||
|
final hasInProgress = tasks.any(
|
||||||
|
(task) => task.status == DriveTaskStatus.inProgress,
|
||||||
|
);
|
||||||
|
final hasPending = tasks.any(
|
||||||
|
(task) => task.status == DriveTaskStatus.pending,
|
||||||
|
);
|
||||||
|
final hasPaused = tasks.any(
|
||||||
|
(task) => task.status == DriveTaskStatus.paused,
|
||||||
|
);
|
||||||
|
final hasFailed = tasks.any(
|
||||||
|
(task) =>
|
||||||
|
task.status == DriveTaskStatus.failed ||
|
||||||
|
task.status == DriveTaskStatus.cancelled ||
|
||||||
|
task.status == DriveTaskStatus.expired,
|
||||||
|
);
|
||||||
|
final hasCompleted = tasks.any(
|
||||||
|
(task) => task.status == DriveTaskStatus.completed,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Priority order: in progress > pending > paused > failed > completed
|
||||||
|
if (hasInProgress) {
|
||||||
|
return '${tasks.length} ${'uploading'.tr()}';
|
||||||
|
} else if (hasPending) {
|
||||||
|
return '${tasks.length} ${'pending'.tr()}';
|
||||||
|
} else if (hasPaused) {
|
||||||
|
return '${tasks.length} ${'paused'.tr()}';
|
||||||
|
} else if (hasFailed) {
|
||||||
|
return '${tasks.length} ${'failed'.tr()}';
|
||||||
|
} else if (hasCompleted) {
|
||||||
|
return '${tasks.length} ${'completed'.tr()}';
|
||||||
|
} else {
|
||||||
|
return '${tasks.length} ${'tasks'.tr()}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasCompletedTasks(List<DriveTask> tasks) {
|
||||||
|
return tasks.any(
|
||||||
|
(task) =>
|
||||||
|
task.status == DriveTaskStatus.completed ||
|
||||||
|
task.status == DriveTaskStatus.failed ||
|
||||||
|
task.status == DriveTaskStatus.cancelled ||
|
||||||
|
task.status == DriveTaskStatus.expired,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UploadTaskTile extends StatefulWidget {
|
||||||
|
final DriveTask task;
|
||||||
|
|
||||||
|
const UploadTaskTile({super.key, required this.task});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<UploadTaskTile> createState() => _UploadTaskTileState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UploadTaskTileState extends State<UploadTaskTile>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
late AnimationController _rotationController;
|
||||||
|
late Animation<double> _rotationAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_rotationController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
_rotationAnimation = Tween<double>(begin: 0.0, end: 0.5).animate(
|
||||||
|
CurvedAnimation(parent: _rotationController, curve: Curves.easeInOut),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_rotationController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ExpansionTile(
|
||||||
|
leading: _buildStatusIcon(context),
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.task.fileName,
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
_formatFileSize(widget.task.fileSize),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(2),
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: widget.task.progress,
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _rotationAnimation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Transform.rotate(
|
||||||
|
angle: _rotationAnimation.value * math.pi,
|
||||||
|
child: Icon(
|
||||||
|
Symbols.expand_more,
|
||||||
|
size: 20,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
tilePadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
onExpansionChanged: (expanded) {
|
||||||
|
if (expanded) {
|
||||||
|
_rotationController.forward();
|
||||||
|
} else {
|
||||||
|
_rotationController.reverse();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 4, 12, 12),
|
||||||
|
child: _buildExpandedDetails(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusIcon(BuildContext context) {
|
||||||
|
IconData icon;
|
||||||
|
Color color;
|
||||||
|
|
||||||
|
switch (widget.task.status) {
|
||||||
|
case DriveTaskStatus.pending:
|
||||||
|
icon = Symbols.schedule;
|
||||||
|
color = Theme.of(context).colorScheme.secondary;
|
||||||
|
break;
|
||||||
|
case DriveTaskStatus.inProgress:
|
||||||
|
icon = Symbols.upload;
|
||||||
|
color = Theme.of(context).colorScheme.primary;
|
||||||
|
break;
|
||||||
|
case DriveTaskStatus.paused:
|
||||||
|
icon = Symbols.pause_circle;
|
||||||
|
color = Theme.of(context).colorScheme.tertiary;
|
||||||
|
break;
|
||||||
|
case DriveTaskStatus.completed:
|
||||||
|
icon = Symbols.check_circle;
|
||||||
|
color = Colors.green;
|
||||||
|
break;
|
||||||
|
case DriveTaskStatus.failed:
|
||||||
|
icon = Symbols.error;
|
||||||
|
color = Theme.of(context).colorScheme.error;
|
||||||
|
break;
|
||||||
|
case DriveTaskStatus.cancelled:
|
||||||
|
icon = Symbols.cancel;
|
||||||
|
color = Theme.of(context).colorScheme.error;
|
||||||
|
break;
|
||||||
|
case DriveTaskStatus.expired:
|
||||||
|
icon = Symbols.timer_off;
|
||||||
|
color = Theme.of(context).colorScheme.error;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Icon(icon, size: 24, 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: switch (widget.task.type) {
|
||||||
|
'FileUpload' => _buildFileUploadDetails(context),
|
||||||
|
_ => _buildGenericTaskDetails(context),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFileUploadDetails(BuildContext context) {
|
||||||
|
final transmissionProgress = widget.task.transmissionProgress ?? 0.0;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Server Processing Progress
|
||||||
|
Text(
|
||||||
|
widget.task.statusMessage ?? 'Processing',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${(widget.task.progress * 100).toStringAsFixed(1)}%',
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${widget.task.uploadedChunks}/${widget.task.totalChunks} chunks',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value: widget.task.progress,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
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 * widget.task.fileSize).toInt())} / ${_formatFileSize(widget.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),
|
||||||
|
|
||||||
|
// Speed and ETA
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_formatBytesPerSecond(widget.task),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.task.status == DriveTaskStatus.inProgress)
|
||||||
|
Text(
|
||||||
|
'ETA: ${_formatDuration(widget.task.estimatedTimeRemaining)}',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Error message if failed
|
||||||
|
if (widget.task.errorMessage != null) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
widget.task.errorMessage!,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGenericTaskDetails(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Generic task progress
|
||||||
|
Text(
|
||||||
|
'Progress',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${(widget.task.progress * 100).toStringAsFixed(1)}%',
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.task.status.name,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value: widget.task.progress,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Error message if failed
|
||||||
|
if (widget.task.errorMessage != null) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
widget.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(DriveTask 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,19 +19,19 @@ PODS:
|
|||||||
- Firebase/Messaging (12.4.0):
|
- Firebase/Messaging (12.4.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseMessaging (~> 12.4.0)
|
- FirebaseMessaging (~> 12.4.0)
|
||||||
- firebase_analytics (12.0.3):
|
- firebase_analytics (12.0.4):
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- FirebaseAnalytics (= 12.4.0)
|
- FirebaseAnalytics (= 12.4.0)
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- firebase_core (4.2.0):
|
- firebase_core (4.2.1):
|
||||||
- Firebase/CoreOnly (~> 12.4.0)
|
- Firebase/CoreOnly (~> 12.4.0)
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- firebase_crashlytics (5.0.3):
|
- firebase_crashlytics (5.0.4):
|
||||||
- Firebase/CoreOnly (~> 12.4.0)
|
- Firebase/CoreOnly (~> 12.4.0)
|
||||||
- Firebase/Crashlytics (~> 12.4.0)
|
- Firebase/Crashlytics (~> 12.4.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- firebase_messaging (16.0.3):
|
- firebase_messaging (16.0.4):
|
||||||
- Firebase/CoreOnly (~> 12.4.0)
|
- Firebase/CoreOnly (~> 12.4.0)
|
||||||
- Firebase/Messaging (~> 12.4.0)
|
- Firebase/Messaging (~> 12.4.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
@@ -416,10 +416,10 @@ SPEC CHECKSUMS:
|
|||||||
file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f
|
file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f
|
||||||
file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7
|
file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7
|
||||||
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||||
firebase_analytics: d876586269c1d8d2b3dcac085bc2d97c62abc9df
|
firebase_analytics: 09241c4796c1c42a02349ef8bf30025f5b640f0e
|
||||||
firebase_core: d81d1a44df95699ce074ae63d8cb43e9df21e142
|
firebase_core: e054894ab56033ef9bcbe2d9eac9395e5306e2fc
|
||||||
firebase_crashlytics: 723622cc39a9fa7320585424f5864c5699893ce1
|
firebase_crashlytics: c2438b5f5bdcacf59d0eaee5852c6b0ab09dab77
|
||||||
firebase_messaging: 31f412ae5a54e02d1c46d467969f7ad92c4b81ec
|
firebase_messaging: 373ac3a56e5aa37bb9aff4127f700aa5973c1168
|
||||||
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||||
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||||
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018
|
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
version: "4.1.0"
|
||||||
convert:
|
convert:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: convert
|
name: convert
|
||||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||||
@@ -1185,10 +1185,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: d8f590a69729f719177ea68eb1e598295e8dbc41bbc247fed78b2c8a25660d7c
|
sha256: c92d18e1fe994cb06d48aa786c46b142a5633067e8297cff6b5a3ac742620104
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "16.3.0"
|
version: "17.0.0"
|
||||||
google_fonts:
|
google_fonts:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ dependencies:
|
|||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
flutter_hooks: ^0.21.3+1
|
flutter_hooks: ^0.21.3+1
|
||||||
hooks_riverpod: ^2.6.1
|
hooks_riverpod: ^2.6.1
|
||||||
go_router: ^16.3.0
|
go_router: ^17.0.0
|
||||||
styled_widget: ^0.4.1
|
styled_widget: ^0.4.1
|
||||||
shared_preferences: ^2.5.3
|
shared_preferences: ^2.5.3
|
||||||
flutter_riverpod: ^2.6.1
|
flutter_riverpod: ^2.6.1
|
||||||
@@ -165,6 +165,7 @@ dependencies:
|
|||||||
dio_smart_retry: ^7.0.1
|
dio_smart_retry: ^7.0.1
|
||||||
flutter_expandable_fab: ^2.5.2
|
flutter_expandable_fab: ^2.5.2
|
||||||
event_bus: ^2.0.1
|
event_bus: ^2.0.1
|
||||||
|
convert: ^3.1.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user