♻️ Better task overlay progress

This commit is contained in:
2026-01-11 13:11:15 +08:00
parent bf59108569
commit 88c4d648d5
2 changed files with 196 additions and 94 deletions

View File

@@ -256,6 +256,21 @@ class UploadTasksNotifier extends Notifier<List<DriveTask>> {
}).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);
}
}
}

View File

@@ -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<Color>(
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<Color>(
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<DriveTask> tasks) {
if (tasks.isEmpty) return 0.0;
final totalProgress = tasks.fold<double>(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<double>(
0.0,
(sum, progress) => sum + progress!,
);
return totalProgress / tasks.length;
}
@@ -625,6 +691,18 @@ class UploadTaskTile extends StatefulWidget {
@override
State<UploadTaskTile> 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<UploadTaskTile>
@@ -685,9 +763,7 @@ class _UploadTaskTileState extends State<UploadTaskTile>
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<UploadTaskTile>
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: widget.task.progress,
value: UploadTaskTile._getTaskProgress(widget.task),
backgroundColor: Theme.of(context).colorScheme.surface,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
@@ -974,4 +1050,4 @@ class _UploadTaskTileState extends State<UploadTaskTile>
return '${duration.inSeconds}s';
}
}
}
}