I have a list of download jobs (e.g. {{url, filename} ...}) to do:

jobs = Table[{"https://picsum.photos/200/300/?random",

Clearly the ProgressIndicator here does not show the percent completion of the asynchronous tasks:

ProgressIndicator[Dynamic[i], {0, 5}]]

So I'm looking for a nice way to monitor the total progress of all tasks created in a call like this:

without resorting to the obsolete symbols URLFetchAsynchronous and URLSaveAsynchronous.

manifest = <||>;

manifest = <|manifest, event["Task"] -> event|>

manifest = <|manifest, event["Task"] -> event|>

SetAttributes[startJob, HoldFirst]
src, dest,
HandlerFunctions -> <|
"ConnectionFailed" -> connectionFailed[manifest],
|>,
HandlerFunctionsKeys -> {
}
];

We have defined a variable, manifest, that will hold information about the files being downloaded. It is up to the user to define the event handler functions that they want to use; in my download manager, I will only use TaskProgress and TaskFinished. Whenever any of those events are called, I update manifest with the latest information. The latest information includes the variables specified under HandleFunctionsKeys.

This is all we need, really. Now we can build an interface to visualize manifest.

manifest = <|
manifest,
|>)

SetAttributes[visualizeManifest, HoldFirst]
visualizeManifest[manifest_] := TableForm[Join[
"Status", ""}}, {
#File
, Floor[#ByteCountTotal/10^6]
, ProgressIndicator[#FractionComplete]
} & /@ Values[manifest]
]]

I will also add a button to begin downloading an Anaconda installer. Anaconda is a software for Python programmers that I picked because the installer is large enough in size that the download won't finish in a blip.

i = 0;
"https://repo.anaconda.com/archive/Anaconda3-5.2.0-MacOSX-x86_64.pkg",
]]

Dynamic@visualizeManifest[manifest]

The final result looks like this:

You can easily compute other statistics such as how many of the files have finished downloading by going through the values in the manifest association.

Here's another way to do this, based on extracting the "File" handler key

DynamicModule[
{jobs, results},
Dynamic[
InternalLoadingPanel@
Grid@
If[AllTrue[Values@results, # =!= None &],
Append[
{
Button[
"Get Result",
NotebookWrite[
Nest[ParentBox, EvaluationBox[], 5],
ToBoxes@Values@results
]
],
SpanFromLeft
}
],
Identity
]@
KeyValueMap[
{
Row@{#[[1]], ":"},
If[#2 =!= None, "Complete", "Waiting..."]
} &,
results
]
],
Initialization :>
{
results = <||>,
jobs = <||>,
Map[
With[{job = Flatten[{#}]},
results[job] = None;
jobs[job] =
Sequence @@ job,
HandlerFunctions ->
HandlerFunctionsKeys -> {"File"}
]
] &,
things
]
}
]

If we run that on the jobs it pops a little watching interface:

After that's done it shows a button to replace the box with the result:

And pressing the button gives us the desired result:

{
}
• I always wonder why functions like InternalLoadingPanel are not in the supported language. (+1) – Edmund Jul 14 '18 at 2:58
• @Edmund I'd almost rather they not be... the "System" context is bloated enough as is. Why they have to be in "Internal" though is beyond me. Some type of supported "Formatting`" context would be perfect for it. Or even better, some context that makes it clear that there's a front-end kernel separation. Unfortunately we're too far past the point of no-return for that. – b3m2a1 Jul 16 '18 at 17:50

With jobs as defined in OP.

completedCount = 0;
HandlerFunctions -> <|"TaskFinished" -> (completedCount++ &)|>
] & @@@ jobs;

This outputs a ProgressIndicator that tracks the completion of the tasks.

Hope this helps.