♻️ Better task overlay progress
This commit is contained in:
@@ -256,6 +256,21 @@ class UploadTasksNotifier extends Notifier<List<DriveTask>> {
|
|||||||
}).toList();
|
}).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(
|
void updateDownloadProgress(
|
||||||
String taskId,
|
String taskId,
|
||||||
int downloadedBytes,
|
int downloadedBytes,
|
||||||
@@ -470,6 +485,7 @@ class EnhancedFileUploader extends FileUploader {
|
|||||||
|
|
||||||
// Step 2: Upload chunks
|
// Step 2: Upload chunks
|
||||||
int bytesUploaded = 0;
|
int bytesUploaded = 0;
|
||||||
|
int chunksUploaded = 0;
|
||||||
if (fileData is XFile) {
|
if (fileData is XFile) {
|
||||||
// Use stream for XFile
|
// Use stream for XFile
|
||||||
final subscription = fileData.openRead().listen(null);
|
final subscription = fileData.openRead().listen(null);
|
||||||
@@ -494,6 +510,11 @@ class EnhancedFileUploader extends FileUploader {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
bytesUploaded += chunkData.length;
|
bytesUploaded += chunkData.length;
|
||||||
|
chunksUploaded += 1;
|
||||||
|
// Update upload progress in UI
|
||||||
|
ref
|
||||||
|
.read(uploadTasksProvider.notifier)
|
||||||
|
.updateUploadProgress(taskId, bytesUploaded, chunksUploaded);
|
||||||
}
|
}
|
||||||
subscription.cancel();
|
subscription.cancel();
|
||||||
} else if (fileData is Uint8List) {
|
} else if (fileData is Uint8List) {
|
||||||
@@ -521,6 +542,11 @@ class EnhancedFileUploader extends FileUploader {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
bytesUploaded += chunks[i].length;
|
bytesUploaded += chunks[i].length;
|
||||||
|
chunksUploaded += 1;
|
||||||
|
// Update upload progress in UI
|
||||||
|
ref
|
||||||
|
.read(uploadTasksProvider.notifier)
|
||||||
|
.updateUploadProgress(taskId, bytesUploaded, chunksUploaded);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw ArgumentError('Invalid fileData type');
|
throw ArgumentError('Invalid fileData type');
|
||||||
|
|||||||
@@ -251,7 +251,9 @@ class _TaskOverlayContent extends HookConsumerWidget {
|
|||||||
style: Theme.of(context).textTheme.bodySmall
|
style: Theme.of(context).textTheme.bodySmall
|
||||||
?.copyWith(
|
?.copyWith(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@@ -260,7 +262,10 @@ class _TaskOverlayContent extends HookConsumerWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
child: CircularProgressIndicator(
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(
|
||||||
value: _getOverallProgress(activeTasks),
|
value: _getOverallProgress(activeTasks),
|
||||||
strokeWidth: 3,
|
strokeWidth: 3,
|
||||||
backgroundColor: Theme.of(
|
backgroundColor: Theme.of(
|
||||||
@@ -268,6 +273,24 @@ class _TaskOverlayContent extends HookConsumerWidget {
|
|||||||
).colorScheme.surfaceContainerHighest,
|
).colorScheme.surfaceContainerHighest,
|
||||||
padding: EdgeInsets.zero,
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 12),
|
).padding(horizontal: 12),
|
||||||
@@ -316,7 +339,9 @@ class _TaskOverlayContent extends HookConsumerWidget {
|
|||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleSmall
|
.titleSmall
|
||||||
?.copyWith(fontWeight: FontWeight.w600),
|
?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@@ -341,13 +366,35 @@ class _TaskOverlayContent extends HookConsumerWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
child: CircularProgressIndicator(
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(
|
||||||
value: _getOverallProgress(activeTasks),
|
value: _getOverallProgress(activeTasks),
|
||||||
strokeWidth: 3,
|
strokeWidth: 3,
|
||||||
backgroundColor: Theme.of(
|
backgroundColor: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.surfaceContainerHighest,
|
).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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Expand/collapse button
|
// Expand/collapse button
|
||||||
@@ -378,13 +425,17 @@ class _TaskOverlayContent extends HookConsumerWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
top: BorderSide(
|
top: BorderSide(
|
||||||
color: Theme.of(context).colorScheme.outline,
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.outline,
|
||||||
width:
|
width:
|
||||||
1 /
|
1 /
|
||||||
MediaQuery.of(context).devicePixelRatio,
|
MediaQuery.of(context).devicePixelRatio,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
// Clear completed tasks button
|
// Clear completed tasks button
|
||||||
@@ -392,7 +443,9 @@ class _TaskOverlayContent extends HookConsumerWidget {
|
|||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
title: const Text('clearCompleted').tr(),
|
title: const Text(
|
||||||
|
'clearCompleted',
|
||||||
|
).tr(),
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
Symbols.clear_all,
|
Symbols.clear_all,
|
||||||
size: 18,
|
size: 18,
|
||||||
@@ -414,7 +467,8 @@ class _TaskOverlayContent extends HookConsumerWidget {
|
|||||||
// Clear all tasks button
|
// Clear all tasks button
|
||||||
if (activeTasks.any(
|
if (activeTasks.any(
|
||||||
(task) =>
|
(task) =>
|
||||||
task.status != DriveTaskStatus.completed,
|
task.status !=
|
||||||
|
DriveTaskStatus.completed,
|
||||||
))
|
))
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
@@ -457,6 +511,7 @@ class _TaskOverlayContent extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -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) {
|
double _getOverallProgress(List<DriveTask> tasks) {
|
||||||
if (tasks.isEmpty) return 0.0;
|
if (tasks.isEmpty) return 0.0;
|
||||||
final totalProgress = tasks.fold<double>(0.0, (sum, task) {
|
|
||||||
final byteProgress = task.fileSize > 0
|
final progressValues = tasks.map((task) => _getTaskProgress(task));
|
||||||
? task.uploadedBytes / task.fileSize
|
final determinateProgresses = progressValues.where((p) => p != null);
|
||||||
: 0.0;
|
|
||||||
return sum +
|
if (determinateProgresses.isEmpty) return 0.0;
|
||||||
(task.status == DriveTaskStatus.inProgress
|
|
||||||
? byteProgress
|
final totalProgress = determinateProgresses.fold<double>(
|
||||||
: task.status == DriveTaskStatus.completed
|
0.0,
|
||||||
? 1
|
(sum, progress) => sum + progress!,
|
||||||
: 0);
|
);
|
||||||
});
|
|
||||||
return totalProgress / tasks.length;
|
return totalProgress / tasks.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,6 +691,18 @@ class UploadTaskTile extends StatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
State<UploadTaskTile> createState() => _UploadTaskTileState();
|
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>
|
class _UploadTaskTileState extends State<UploadTaskTile>
|
||||||
@@ -685,9 +763,7 @@ class _UploadTaskTileState extends State<UploadTaskTile>
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(2),
|
padding: const EdgeInsets.all(2),
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
value: widget.task.fileSize > 0
|
value: UploadTaskTile._getTaskProgress(widget.task),
|
||||||
? widget.task.uploadedBytes / widget.task.fileSize
|
|
||||||
: 0.0,
|
|
||||||
strokeWidth: 2.5,
|
strokeWidth: 2.5,
|
||||||
backgroundColor: Theme.of(
|
backgroundColor: Theme.of(
|
||||||
context,
|
context,
|
||||||
@@ -814,7 +890,7 @@ class _UploadTaskTileState extends State<UploadTaskTile>
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
value: widget.task.progress,
|
value: UploadTaskTile._getTaskProgress(widget.task),
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
Theme.of(context).colorScheme.primary,
|
Theme.of(context).colorScheme.primary,
|
||||||
|
|||||||
Reference in New Issue
Block a user