🚚 Update files layout of pods
This commit is contained in:
185
lib/pods/drive/file_list.dart
Normal file
185
lib/pods/drive/file_list.dart
Normal file
@@ -0,0 +1,185 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/file_list_item.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/paging.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'file_list.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get('/drive/billing/usage');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
final indexedCloudFileListProvider = AsyncNotifierProvider(
|
||||
IndexedCloudFileListNotifier.new,
|
||||
);
|
||||
|
||||
class IndexedCloudFileListNotifier extends AsyncNotifier<List<FileListItem>>
|
||||
with AsyncPaginationController<FileListItem> {
|
||||
String _currentPath = '/';
|
||||
String? _poolId;
|
||||
String? _query;
|
||||
String? _order;
|
||||
bool _orderDesc = false;
|
||||
|
||||
void setPath(String path) {
|
||||
_currentPath = path;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setPool(String? poolId) {
|
||||
_poolId = poolId;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setQuery(String? query) {
|
||||
_query = query;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setOrder(String? order) {
|
||||
_order = order;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setOrderDesc(bool orderDesc) {
|
||||
_orderDesc = orderDesc;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<FileListItem>> fetch() async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
final queryParameters = <String, String>{'path': _currentPath};
|
||||
|
||||
if (_poolId != null) {
|
||||
queryParameters['pool'] = _poolId!;
|
||||
}
|
||||
|
||||
if (_query != null) {
|
||||
queryParameters['query'] = _query!;
|
||||
}
|
||||
|
||||
if (_order != null) {
|
||||
queryParameters['order'] = _order!;
|
||||
}
|
||||
|
||||
queryParameters['orderDesc'] = _orderDesc.toString();
|
||||
|
||||
final response = await client.get(
|
||||
'/drive/index/browse',
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
|
||||
final List<String> folders =
|
||||
(response.data['folders'] as List).map((e) => e as String).toList();
|
||||
final List<SnCloudFileIndex> files =
|
||||
(response.data['files'] as List)
|
||||
.map((e) => SnCloudFileIndex.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final List<FileListItem> items = [
|
||||
...folders.map((folderName) => FileListItem.folder(folderName)),
|
||||
...files.map((file) => FileListItem.file(file)),
|
||||
];
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
final unindexedFileListProvider = AsyncNotifierProvider(
|
||||
UnindexedFileListNotifier.new,
|
||||
);
|
||||
|
||||
class UnindexedFileListNotifier extends AsyncNotifier<List<FileListItem>>
|
||||
with AsyncPaginationController<FileListItem> {
|
||||
String? _poolId;
|
||||
bool _recycled = false;
|
||||
String? _query;
|
||||
String? _order;
|
||||
bool _orderDesc = false;
|
||||
|
||||
void setPool(String? poolId) {
|
||||
_poolId = poolId;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setRecycled(bool recycled) {
|
||||
_recycled = recycled;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setQuery(String? query) {
|
||||
_query = query;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setOrder(String? order) {
|
||||
_order = order;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setOrderDesc(bool orderDesc) {
|
||||
_orderDesc = orderDesc;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
static const int pageSize = 20;
|
||||
|
||||
@override
|
||||
Future<List<FileListItem>> fetch() async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
final queryParameters = <String, String>{
|
||||
'take': pageSize.toString(),
|
||||
'offset': fetchedCount.toString(),
|
||||
};
|
||||
|
||||
if (_poolId != null) {
|
||||
queryParameters['pool'] = _poolId!;
|
||||
}
|
||||
|
||||
if (_recycled) {
|
||||
queryParameters['recycled'] = _recycled.toString();
|
||||
}
|
||||
|
||||
if (_query != null) {
|
||||
queryParameters['query'] = _query!;
|
||||
}
|
||||
|
||||
if (_order != null) {
|
||||
queryParameters['order'] = _order!;
|
||||
}
|
||||
|
||||
queryParameters['orderDesc'] = _orderDesc.toString();
|
||||
|
||||
final response = await client.get(
|
||||
'/drive/index/unindexed',
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
|
||||
totalCount = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
|
||||
|
||||
final List<SnCloudFile> files =
|
||||
(response.data as List)
|
||||
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final List<FileListItem> items =
|
||||
files.map((file) => FileListItem.unindexedFile(file)).toList();
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<Map<String, dynamic>?> billingQuota(Ref ref) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get('/drive/billing/quota');
|
||||
return response.data;
|
||||
}
|
||||
92
lib/pods/drive/file_list.g.dart
Normal file
92
lib/pods/drive/file_list.g.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'file_list.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(billingUsage)
|
||||
const billingUsageProvider = BillingUsageProvider._();
|
||||
|
||||
final class BillingUsageProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<Map<String, dynamic>?>,
|
||||
Map<String, dynamic>?,
|
||||
FutureOr<Map<String, dynamic>?>
|
||||
>
|
||||
with
|
||||
$FutureModifier<Map<String, dynamic>?>,
|
||||
$FutureProvider<Map<String, dynamic>?> {
|
||||
const BillingUsageProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'billingUsageProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$billingUsageHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<Map<String, dynamic>?> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<Map<String, dynamic>?> create(Ref ref) {
|
||||
return billingUsage(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$billingUsageHash() => r'58d8bc774868d60781574c85d6b25869a79c57aa';
|
||||
|
||||
@ProviderFor(billingQuota)
|
||||
const billingQuotaProvider = BillingQuotaProvider._();
|
||||
|
||||
final class BillingQuotaProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<Map<String, dynamic>?>,
|
||||
Map<String, dynamic>?,
|
||||
FutureOr<Map<String, dynamic>?>
|
||||
>
|
||||
with
|
||||
$FutureModifier<Map<String, dynamic>?>,
|
||||
$FutureProvider<Map<String, dynamic>?> {
|
||||
const BillingQuotaProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'billingQuotaProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$billingQuotaHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<Map<String, dynamic>?> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<Map<String, dynamic>?> create(Ref ref) {
|
||||
return billingQuota(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$billingQuotaHash() => r'4ec5d728e439015800abb2d0d673b5a7329cc654';
|
||||
24
lib/pods/drive/file_pool.dart
Normal file
24
lib/pods/drive/file_pool.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file_pool.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
|
||||
final poolsProvider = FutureProvider<List<SnFilePool>>((ref) async {
|
||||
final dio = ref.watch(apiClientProvider);
|
||||
final response = await dio.get('/drive/pools');
|
||||
return response.data
|
||||
.map((e) => SnFilePool.fromJson(e))
|
||||
.cast<SnFilePool>()
|
||||
.toList();
|
||||
});
|
||||
|
||||
String? resolveDefaultPoolId(WidgetRef ref, List<SnFilePool> pools) {
|
||||
final settings = ref.watch(appSettingsProvider);
|
||||
|
||||
final configuredId = settings.defaultPoolId;
|
||||
if (configuredId != null && pools.any((p) => p.id == configuredId)) {
|
||||
return configuredId;
|
||||
}
|
||||
|
||||
return pools.firstOrNull?.id;
|
||||
}
|
||||
15
lib/pods/drive/file_references.dart
Normal file
15
lib/pods/drive/file_references.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:island/models/reference.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
|
||||
part 'file_references.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<Reference>> fileReferences(Ref ref, String fileId) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get('/drive/files/$fileId/references');
|
||||
final list = response.data as List<dynamic>;
|
||||
return list
|
||||
.map((json) => Reference.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
85
lib/pods/drive/file_references.g.dart
Normal file
85
lib/pods/drive/file_references.g.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'file_references.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(fileReferences)
|
||||
const fileReferencesProvider = FileReferencesFamily._();
|
||||
|
||||
final class FileReferencesProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<Reference>>,
|
||||
List<Reference>,
|
||||
FutureOr<List<Reference>>
|
||||
>
|
||||
with $FutureModifier<List<Reference>>, $FutureProvider<List<Reference>> {
|
||||
const FileReferencesProvider._({
|
||||
required FileReferencesFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'fileReferencesProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$fileReferencesHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'fileReferencesProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<Reference>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<Reference>> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return fileReferences(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is FileReferencesProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$fileReferencesHash() => r'd66c678c221f61978bdb242b98e6dbe31d0c204b';
|
||||
|
||||
final class FileReferencesFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<List<Reference>>, String> {
|
||||
const FileReferencesFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'fileReferencesProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
FileReferencesProvider call(String fileId) =>
|
||||
FileReferencesProvider._(argument: fileId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'fileReferencesProvider';
|
||||
}
|
||||
533
lib/pods/drive/upload_tasks.dart
Normal file
533
lib/pods/drive/upload_tasks.dart
Normal file
@@ -0,0 +1,533 @@
|
||||
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 = NotifierProvider(UploadTasksNotifier.new);
|
||||
|
||||
class UploadTasksNotifier extends Notifier<List<DriveTask>> {
|
||||
StreamSubscription? _websocketSubscription;
|
||||
final Map<String, Map<String, dynamic>> _pendingUploads = {};
|
||||
|
||||
@override
|
||||
List<DriveTask> build() {
|
||||
_listenToWebSocket();
|
||||
return [];
|
||||
}
|
||||
|
||||
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 updateDownloadProgress(
|
||||
String taskId,
|
||||
int downloadedBytes,
|
||||
int totalBytes,
|
||||
) {
|
||||
state =
|
||||
state.map((task) {
|
||||
if (task.taskId == taskId) {
|
||||
return task.copyWith(
|
||||
fileSize: totalBytes,
|
||||
uploadedBytes: downloadedBytes,
|
||||
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();
|
||||
}
|
||||
|
||||
void clearAllTasks() {
|
||||
state = [];
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
String addLocalDownloadTask(SnCloudFile item) {
|
||||
final taskId =
|
||||
'download-${item.id}-${DateTime.now().millisecondsSinceEpoch}';
|
||||
final task = DriveTask(
|
||||
id: taskId,
|
||||
taskId: taskId,
|
||||
fileName: item.name,
|
||||
contentType: item.mimeType ?? '',
|
||||
fileSize: 0,
|
||||
uploadedBytes: 0,
|
||||
totalChunks: 1,
|
||||
uploadedChunks: 0,
|
||||
status: DriveTaskStatus.inProgress,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
type: 'FileDownload',
|
||||
);
|
||||
state = [...state, task];
|
||||
return taskId;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_websocketSubscription?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
String? path,
|
||||
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,
|
||||
path: path,
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user