diff --git a/lib/pods/drive/upload_tasks.dart b/lib/pods/drive/upload_tasks.dart index da76a1cd..5f13bdae 100644 --- a/lib/pods/drive/upload_tasks.dart +++ b/lib/pods/drive/upload_tasks.dart @@ -256,6 +256,21 @@ class UploadTasksNotifier extends Notifier> { }).toList(); } + void updateUploadProgress(String taskId, int uploadedBytes, int uploadedChunks) { + state = + state.map((task) { + if (task.taskId == taskId) { + return task.copyWith( + uploadedBytes: uploadedBytes, + uploadedChunks: uploadedChunks, + status: DriveTaskStatus.inProgress, + updatedAt: DateTime.now(), + ); + } + return task; + }).toList(); + } + void updateDownloadProgress( String taskId, int downloadedBytes, @@ -470,6 +485,7 @@ class EnhancedFileUploader extends FileUploader { // Step 2: Upload chunks int bytesUploaded = 0; + int chunksUploaded = 0; if (fileData is XFile) { // Use stream for XFile final subscription = fileData.openRead().listen(null); @@ -494,6 +510,11 @@ class EnhancedFileUploader extends FileUploader { }, ); bytesUploaded += chunkData.length; + chunksUploaded += 1; + // Update upload progress in UI + ref + .read(uploadTasksProvider.notifier) + .updateUploadProgress(taskId, bytesUploaded, chunksUploaded); } subscription.cancel(); } else if (fileData is Uint8List) { @@ -521,6 +542,11 @@ class EnhancedFileUploader extends FileUploader { }, ); bytesUploaded += chunks[i].length; + chunksUploaded += 1; + // Update upload progress in UI + ref + .read(uploadTasksProvider.notifier) + .updateUploadProgress(taskId, bytesUploaded, chunksUploaded); } } else { throw ArgumentError('Invalid fileData type'); @@ -530,4 +556,4 @@ class EnhancedFileUploader extends FileUploader { onProgress?.call(null, Duration.zero); return await completeUpload(taskId); } -} +} \ No newline at end of file diff --git a/lib/widgets/task_overlay.dart b/lib/widgets/task_overlay.dart index 5a01285f..00e73bac 100644 --- a/lib/widgets/task_overlay.dart +++ b/lib/widgets/task_overlay.dart @@ -251,7 +251,9 @@ class _TaskOverlayContent extends HookConsumerWidget { style: Theme.of(context).textTheme.bodySmall ?.copyWith( fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface, + color: Theme.of( + context, + ).colorScheme.onSurface, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -260,13 +262,34 @@ class _TaskOverlayContent extends HookConsumerWidget { SizedBox( width: 16, height: 16, - child: CircularProgressIndicator( - value: _getOverallProgress(activeTasks), - strokeWidth: 3, - backgroundColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - padding: EdgeInsets.zero, + child: Stack( + alignment: Alignment.center, + children: [ + CircularProgressIndicator( + value: _getOverallProgress(activeTasks), + strokeWidth: 3, + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + padding: EdgeInsets.zero, + ), + if (activeTasks.any( + (task) => + task.status == DriveTaskStatus.inProgress, + )) + CircularProgressIndicator( + value: null, // Indeterminate + strokeWidth: 3, + trackGap: 0, + valueColor: AlwaysStoppedAnimation( + Theme.of( + context, + ).colorScheme.secondary.withOpacity(0.5), + ), + backgroundColor: Colors.transparent, + padding: EdgeInsets.zero, + ), + ], ), ), ], @@ -316,7 +339,9 @@ class _TaskOverlayContent extends HookConsumerWidget { style: Theme.of(context) .textTheme .titleSmall - ?.copyWith(fontWeight: FontWeight.w600), + ?.copyWith( + fontWeight: FontWeight.w600, + ), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -341,12 +366,34 @@ class _TaskOverlayContent extends HookConsumerWidget { SizedBox( width: 32, height: 32, - child: CircularProgressIndicator( - value: _getOverallProgress(activeTasks), - strokeWidth: 3, - backgroundColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, + child: Stack( + children: [ + CircularProgressIndicator( + value: _getOverallProgress(activeTasks), + strokeWidth: 3, + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + ), + if (activeTasks.any( + (task) => + task.status == + DriveTaskStatus.inProgress, + )) + CircularProgressIndicator( + value: null, // Indeterminate + strokeWidth: 3, + trackGap: 0, + valueColor: + AlwaysStoppedAnimation( + Theme.of(context) + .colorScheme + .secondary + .withOpacity(0.5), + ), + backgroundColor: Colors.transparent, + ), + ], ), ), @@ -378,82 +425,90 @@ class _TaskOverlayContent extends HookConsumerWidget { decoration: BoxDecoration( border: Border( top: BorderSide( - color: Theme.of(context).colorScheme.outline, + 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('clearCompleted').tr(), - leading: Icon( - Symbols.clear_all, - size: 18, - color: Theme.of( + child: Material( + color: Colors.transparent, + child: CustomScrollView( + slivers: [ + // Clear completed tasks button + if (_hasCompletedTasks(activeTasks)) + SliverToBoxAdapter( + child: ListTile( + dense: true, + title: const Text( + 'clearCompleted', + ).tr(), + leading: Icon( + Symbols.clear_all, + size: 18, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + onTap: () { + taskNotifier.clearCompletedTasks(); + onExpansionChanged?.call(false); + }, + + tileColor: Theme.of( context, - ).colorScheme.onSurfaceVariant, + ).colorScheme.surfaceContainerHighest, ), - onTap: () { - taskNotifier.clearCompletedTasks(); - onExpansionChanged?.call(false); - }, - - tileColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, ), - ), - // Clear all tasks button - if (activeTasks.any( - (task) => - task.status != DriveTaskStatus.completed, - )) - SliverToBoxAdapter( - child: ListTile( - dense: true, - title: const Text('Clear All'), - leading: Icon( - Symbols.clear_all, - size: 18, - color: Theme.of( + // Clear all tasks button + if (activeTasks.any( + (task) => + task.status != + DriveTaskStatus.completed, + )) + SliverToBoxAdapter( + child: ListTile( + dense: true, + title: const Text('Clear All'), + leading: Icon( + Symbols.clear_all, + size: 18, + color: Theme.of( + context, + ).colorScheme.error, + ), + onTap: () { + taskNotifier.clearAllTasks(); + onExpansionChanged?.call(false); + }, + tileColor: Theme.of( context, - ).colorScheme.error, + ).colorScheme.surfaceContainerHighest, ), - onTap: () { - taskNotifier.clearAllTasks(); - onExpansionChanged?.call(false); - }, - 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), - ), - ], + // 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), + ), + ], + ), ), ), ), @@ -488,19 +543,30 @@ class _TaskOverlayContent extends HookConsumerWidget { ); } + double? _getTaskProgress(DriveTask task) { + if (task.status == DriveTaskStatus.completed) return 1.0; + if (task.status != DriveTaskStatus.inProgress) return 0.0; + + // If all bytes are uploaded but still in progress, show indeterminate + if (task.uploadedBytes >= task.fileSize && task.fileSize > 0) { + return null; // Indeterminate progress + } + + return task.fileSize > 0 ? task.uploadedBytes / task.fileSize : 0.0; + } + double _getOverallProgress(List tasks) { if (tasks.isEmpty) return 0.0; - final totalProgress = tasks.fold(0.0, (sum, task) { - final byteProgress = task.fileSize > 0 - ? task.uploadedBytes / task.fileSize - : 0.0; - return sum + - (task.status == DriveTaskStatus.inProgress - ? byteProgress - : task.status == DriveTaskStatus.completed - ? 1 - : 0); - }); + + final progressValues = tasks.map((task) => _getTaskProgress(task)); + final determinateProgresses = progressValues.where((p) => p != null); + + if (determinateProgresses.isEmpty) return 0.0; + + final totalProgress = determinateProgresses.fold( + 0.0, + (sum, progress) => sum + progress!, + ); return totalProgress / tasks.length; } @@ -625,6 +691,18 @@ class UploadTaskTile extends StatefulWidget { @override State createState() => _UploadTaskTileState(); + + static double? _getTaskProgress(DriveTask task) { + if (task.status == DriveTaskStatus.completed) return 1.0; + if (task.status == DriveTaskStatus.inProgress) return null; + + // If all bytes are uploaded but still in progress, show indeterminate + if (task.uploadedBytes >= task.fileSize && task.fileSize > 0) { + return null; // Indeterminate progress + } + + return task.fileSize > 0 ? task.uploadedBytes / task.fileSize : 0.0; + } } class _UploadTaskTileState extends State @@ -685,9 +763,7 @@ class _UploadTaskTileState extends State child: Padding( padding: const EdgeInsets.all(2), child: CircularProgressIndicator( - value: widget.task.fileSize > 0 - ? widget.task.uploadedBytes / widget.task.fileSize - : 0.0, + value: UploadTaskTile._getTaskProgress(widget.task), strokeWidth: 2.5, backgroundColor: Theme.of( context, @@ -814,7 +890,7 @@ class _UploadTaskTileState extends State ), const SizedBox(height: 4), LinearProgressIndicator( - value: widget.task.progress, + value: UploadTaskTile._getTaskProgress(widget.task), backgroundColor: Theme.of(context).colorScheme.surface, valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.primary, @@ -974,4 +1050,4 @@ class _UploadTaskTileState extends State return '${duration.inSeconds}s'; } } -} \ No newline at end of file +}