From bcba7184f07d0c50b747cf83e7c5192ccf368a24 Mon Sep 17 00:00:00 2001 From: Jarek Kobus Date: Mon, 27 May 2024 20:02:04 +0200 Subject: [PATCH] TaskTree: Implement AssetDownloader example This is an equivalent of the downloader used for the car-configurator example. Change-Id: Ia0ab13ea7d4557d375b44aa2ba06be5a81651251 Reviewed-by: hjk --- tests/manual/manual.qbs | 1 + tests/manual/tasking/CMakeLists.txt | 1 + .../tasking/assetdownloader/CMakeLists.txt | 8 + .../assetdownloader/assetdownloader.cpp | 532 ++++++++++++++++++ .../tasking/assetdownloader/assetdownloader.h | 102 ++++ .../assetdownloader/assetdownloader.qbs | 13 + tests/manual/tasking/assetdownloader/main.cpp | 50 ++ 7 files changed, 707 insertions(+) create mode 100644 tests/manual/tasking/assetdownloader/CMakeLists.txt create mode 100644 tests/manual/tasking/assetdownloader/assetdownloader.cpp create mode 100644 tests/manual/tasking/assetdownloader/assetdownloader.h create mode 100644 tests/manual/tasking/assetdownloader/assetdownloader.qbs create mode 100644 tests/manual/tasking/assetdownloader/main.cpp diff --git a/tests/manual/manual.qbs b/tests/manual/manual.qbs index 2d84a6095ed..8e4e7575a2d 100644 --- a/tests/manual/manual.qbs +++ b/tests/manual/manual.qbs @@ -13,6 +13,7 @@ Project { "shootout/shootout.qbs", "spinner/spinner.qbs", "subdirfilecontainer/subdirfilecontainer.qbs", + "tasking/assetdownloader/assetdownloader.qbs", "tasking/dataexchange/dataexchange.qbs", "tasking/demo/demo.qbs", "tasking/imagescaling/imagescaling.qbs", diff --git a/tests/manual/tasking/CMakeLists.txt b/tests/manual/tasking/CMakeLists.txt index 196b6acf6af..1a2c6c6dc8a 100644 --- a/tests/manual/tasking/CMakeLists.txt +++ b/tests/manual/tasking/CMakeLists.txt @@ -1,3 +1,4 @@ +add_subdirectory(assetdownloader) add_subdirectory(dataexchange) add_subdirectory(demo) add_subdirectory(imagescaling) diff --git a/tests/manual/tasking/assetdownloader/CMakeLists.txt b/tests/manual/tasking/assetdownloader/CMakeLists.txt new file mode 100644 index 00000000000..a9a5833262b --- /dev/null +++ b/tests/manual/tasking/assetdownloader/CMakeLists.txt @@ -0,0 +1,8 @@ +add_qtc_test(tst_tasking_assetdownloader + MANUALTEST + DEPENDS Tasking Qt::Concurrent Qt::Network Qt::Widgets Qt::GuiPrivate Qt::CorePrivate + SOURCES + assetdownloader.cpp + assetdownloader.h + main.cpp +) diff --git a/tests/manual/tasking/assetdownloader/assetdownloader.cpp b/tests/manual/tasking/assetdownloader/assetdownloader.cpp new file mode 100644 index 00000000000..90fa21837cb --- /dev/null +++ b/tests/manual/tasking/assetdownloader/assetdownloader.cpp @@ -0,0 +1,532 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "assetdownloader.h" + +#include +#include +#include + +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) +#include +#else +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Tasking; + +struct DownloadableAssets +{ + QUrl remoteUrl; + QList allAssets; + QList assetsToDownload; +}; + +class AssetDownloaderPrivate +{ +public: + AssetDownloaderPrivate(AssetDownloader *q) : m_q(q) {} + + AssetDownloader *m_q = nullptr; + TaskTreeRunner m_taskTreeRunner; + QString m_lastProgressText; + + QNetworkAccessManager *m_manager = nullptr; + QString m_jsonFileName; + QString m_zipFileName; + QDir m_preferredLocalDownloadDir = + QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); + QUrl m_offlineAssetsFilePath; + QUrl m_downloadBase; + + void setProgress(int progressValue, int progressMaximum, const QString &progressText) + { + m_lastProgressText = progressText; + emit m_q->progressChanged(progressValue, progressMaximum, progressText); + } + void updateProgress(int progressValue, int progressMaximum) + { + setProgress(progressValue, progressMaximum, m_lastProgressText); + } + void clearProgress(const QString &progressText) + { + setProgress(0, 0, progressText); + } + + void setupDownload(NetworkQuery *query, const QString &progressText) + { + query->setNetworkAccessManager(m_manager); + clearProgress(progressText); + QObject::connect(query, &NetworkQuery::started, query, [this, query] { + QNetworkReply *reply = query->reply(); + QObject::connect(reply, &QNetworkReply::downloadProgress, + query, [this](qint64 bytesReceived, qint64 totalBytes) { + updateProgress((totalBytes > 0) ? 100.0 * bytesReceived / totalBytes : 0, 100); + }); + }); + } +}; + +static bool isWritableDir(const QDir &dir) +{ + if (dir.exists()) { + QTemporaryFile file(dir.filePath(QString::fromLatin1("tmp"))); + return file.open(); + } + return false; +} + +static bool sameFileContent(const QFileInfo &first, const QFileInfo &second) +{ + if (first.exists() ^ second.exists()) + return false; + + if (first.size() != second.size()) + return false; + + QFile firstFile(first.absoluteFilePath()); + QFile secondFile(second.absoluteFilePath()); + + if (firstFile.open(QFile::ReadOnly) && secondFile.open(QFile::ReadOnly)) { + char char1; + char char2; + int readBytes1 = 0; + int readBytes2 = 0; + while (!firstFile.atEnd()) { + readBytes1 = firstFile.read(&char1, 1); + readBytes2 = secondFile.read(&char2, 1); + if (readBytes1 != readBytes2 || readBytes1 != 1) + return false; + if (char1 != char2) + return false; + } + return true; + } + + return false; +} + +static bool createDirectory(const QDir &dir) +{ + if (dir.exists()) + return true; + + if (!createDirectory(dir.absoluteFilePath(QString::fromUtf8("..")))) + return false; + + return dir.mkpath(QString::fromUtf8(".")); +} + +static bool canBeALocalBaseDir(const QDir &dir) +{ + if (dir.exists()) + return !dir.isEmpty() || isWritableDir(dir); + return createDirectory(dir) && isWritableDir(dir); +} + +static QString pathFromUrl(const QUrl &url) +{ + return url.isLocalFile() ? url.toLocalFile() : url.toString(); +} + +static QList filterDownloadableAssets(const QList &assetFiles, const QDir &expectedDir) +{ + QList downloadList; + std::copy_if(assetFiles.begin(), assetFiles.end(), std::back_inserter(downloadList), + [&](const QUrl &assetPath) { + return !QFileInfo::exists(expectedDir.absoluteFilePath(assetPath.toString())); + }); + return downloadList; +} + +static bool allAssetsPresent(const QList &assetFiles, const QDir &expectedDir) +{ + return std::all_of(assetFiles.begin(), assetFiles.end(), [&](const QUrl &assetPath) { + return QFileInfo::exists(expectedDir.absoluteFilePath(assetPath.toString())); + }); +} + +AssetDownloader::AssetDownloader(QObject *parent) + : QObject(parent) + , d(new AssetDownloaderPrivate(this)) +{} + +AssetDownloader::~AssetDownloader() = default; + +void AssetDownloader::setNetworkAccessManager(QNetworkAccessManager *manager) +{ + d->m_manager = manager; +} + +QUrl AssetDownloader::downloadBase() const +{ + return d->m_downloadBase; +} + +void AssetDownloader::setDownloadBase(const QUrl &downloadBase) +{ + if (d->m_downloadBase != downloadBase) { + d->m_downloadBase = downloadBase; + emit downloadBaseChanged(d->m_downloadBase); + } +} + +QUrl AssetDownloader::preferredLocalDownloadDir() const +{ + return QUrl::fromLocalFile(d->m_preferredLocalDownloadDir.absolutePath()); +} + +void AssetDownloader::setPreferredLocalDownloadDir(const QUrl &localDir) +{ + if (!localDir.isLocalFile()) + qWarning() << "preferredLocalDownloadDir Should be a local directory"; + + const QString path = pathFromUrl(localDir); + if (d->m_preferredLocalDownloadDir != path) { + d->m_preferredLocalDownloadDir.setPath(path); + emit preferredLocalDownloadDirChanged(preferredLocalDownloadDir()); + } +} + +QUrl AssetDownloader::offlineAssetsFilePath() const +{ + return d->m_offlineAssetsFilePath; +} + +void AssetDownloader::setOfflineAssetsFilePath(const QUrl &offlineAssetsFilePath) +{ + if (d->m_offlineAssetsFilePath != offlineAssetsFilePath) { + d->m_offlineAssetsFilePath = offlineAssetsFilePath; + emit offlineAssetsFilePathChanged(d->m_offlineAssetsFilePath); + } +} + +QString AssetDownloader::jsonFileName() const +{ + return d->m_jsonFileName; +} + +void AssetDownloader::setJsonFileName(const QString &jsonFileName) +{ + if (d->m_jsonFileName != jsonFileName) { + d->m_jsonFileName = jsonFileName; + emit jsonFileNameChanged(d->m_jsonFileName); + } +} + +QString AssetDownloader::zipFileName() const +{ + return d->m_zipFileName; +} + +void AssetDownloader::setZipFileName(const QString &zipFileName) +{ + if (d->m_zipFileName != zipFileName) { + d->m_zipFileName = zipFileName; + emit zipFileNameChanged(d->m_zipFileName); + } +} + +static QDir baseLocalDir(const QDir &preferredLocalDir) +{ + if (canBeALocalBaseDir(preferredLocalDir)) + return preferredLocalDir; + + qWarning().noquote() << "AssetDownloader: Cannot set \"" << preferredLocalDir + << "\" as a local download directory!"; + return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); +} + +static void precheckLocalFile(const QUrl &url) +{ + if (url.isEmpty()) + return; + QFile file(pathFromUrl(url)); + if (!file.open(QIODevice::ReadOnly)) + qWarning() << "Cannot open local file" << url; +} + +static void readAssetsFileContent(QPromise &promise, const QByteArray &content) +{ + const QJsonObject json = QJsonDocument::fromJson(content).object(); + const QJsonArray assetsArray = json[u"assets"].toArray(); + DownloadableAssets result; + result.remoteUrl = json[u"url"].toString(); + for (const QJsonValue &asset : assetsArray) { + if (promise.isCanceled()) + return; + result.allAssets.append(asset.toString()); + } + result.assetsToDownload = result.allAssets; + + if (result.allAssets.isEmpty() || result.remoteUrl.isEmpty()) + promise.future().cancel(); + else + promise.addResult(result); +} + +static void unzip(QPromise &promise, const QByteArray &content, const QDir &directory, + const QString &fileName) +{ + const QString zipFilePath = directory.absoluteFilePath(fileName); + QFile zipFile(zipFilePath); + if (!zipFile.open(QIODevice::WriteOnly)) { + promise.future().cancel(); + return; + } + zipFile.write(content); + zipFile.close(); + + if (promise.isCanceled()) + return; + + QZipReader reader(zipFilePath); + const bool extracted = reader.extractAll(directory.absolutePath()); + reader.close(); + if (extracted) + QFile::remove(zipFilePath); + else + promise.future().cancel(); +} + +static void writeAsset(QPromise &promise, const QByteArray &content, const QString &filePath) +{ + const QFileInfo fileInfo(filePath); + QFile file(fileInfo.absoluteFilePath()); + if (!createDirectory(fileInfo.dir()) || !file.open(QFile::WriteOnly)) { + promise.future().cancel(); + return; + } + + if (promise.isCanceled()) + return; + + file.write(content); + file.close(); +} + +static void copyAndCheck(QPromise &promise, const QString &sourcePath, const QString &destPath) +{ + QFile sourceFile(sourcePath); + QFile destFile(destPath); + const QFileInfo sourceFileInfo(sourceFile.fileName()); + const QFileInfo destFileInfo(destFile.fileName()); + + if (destFile.exists() && !destFile.remove()) { + promise.future().cancel(); + return; + } + + if (!createDirectory(destFileInfo.absolutePath())) { + promise.future().cancel(); + return; + } + + if (promise.isCanceled()) + return; + + if (!sourceFile.copy(destFile.fileName()) && !sameFileContent(sourceFileInfo, destFileInfo)) + promise.future().cancel(); +} + +void AssetDownloader::start() +{ + if (d->m_taskTreeRunner.isRunning()) + return; + + struct InternalStorage + { + QTemporaryDir temporaryDir; + QDir tempDir; + QDir baseLocalDir; + QByteArray jsonContent; + DownloadableAssets assets; + QByteArray zipContent; + int doneCount = 0; + }; + + const Storage storage; + + const auto onSetup = [this, storage] { + if (!storage->temporaryDir.isValid()) { + qWarning() << "Cannot create a temporary directory."; + return SetupResult::StopWithError; + } + storage->tempDir = storage->temporaryDir.path(); + storage->baseLocalDir = baseLocalDir(d->m_preferredLocalDownloadDir); + precheckLocalFile(d->m_offlineAssetsFilePath); + return SetupResult::Continue; + }; + + const auto onJsonDownloadSetup = [this](NetworkQuery &query) { + query.setRequest(QNetworkRequest(d->m_downloadBase.resolved(d->m_jsonFileName))); + d->setupDownload(&query, tr("Downloading JSON file...")); + }; + const auto onJsonDownloadDone = [this, storage](const NetworkQuery &query, DoneWith result) { + if (result == DoneWith::Success) { + storage->jsonContent = query.reply()->readAll(); + return DoneResult::Success; + } + qWarning() << "Cannot download" << d->m_downloadBase.resolved(d->m_jsonFileName) + << query.reply()->errorString(); + if (d->m_offlineAssetsFilePath.isEmpty()) { + qWarning() << "Also there is no local file as a replacement"; + return DoneResult::Error; + } + + QFile file(pathFromUrl(d->m_offlineAssetsFilePath)); + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "Also failed to open" << d->m_offlineAssetsFilePath; + return DoneResult::Error; + } + + storage->jsonContent = file.readAll(); + return DoneResult::Success; + }; + + const auto onReadAssetsFileSetup = [storage](ConcurrentCall &async) { + async.setConcurrentCallData(readAssetsFileContent, storage->jsonContent); + }; + const auto onReadAssetsFileDone = [storage](const ConcurrentCall &async) { + storage->assets = async.result(); + }; + + const auto onSkipIfAllAssetsPresent = [storage] { + return allAssetsPresent(storage->assets.allAssets, storage->baseLocalDir) + ? SetupResult::StopWithSuccess : SetupResult::Continue; + }; + + const auto onZipDownloadSetup = [this, storage](NetworkQuery &query) { + if (d->m_zipFileName.isEmpty()) + return SetupResult::StopWithSuccess; + + query.setRequest(QNetworkRequest(d->m_downloadBase.resolved(d->m_zipFileName))); + d->setupDownload(&query, tr("Downloading zip file...")); + return SetupResult::Continue; + }; + const auto onZipDownloadDone = [storage](const NetworkQuery &query, DoneWith result) { + if (result == DoneWith::Success) + storage->zipContent = query.reply()->readAll(); + return DoneResult::Success; // Ignore zip download failure + }; + + const auto onUnzipSetup = [this, storage](ConcurrentCall &async) { + if (storage->zipContent.isEmpty()) + return SetupResult::StopWithSuccess; + + async.setConcurrentCallData(unzip, storage->zipContent, storage->tempDir, d->m_zipFileName); + d->clearProgress(tr("Unzipping...")); + return SetupResult::Continue; + }; + const auto onUnzipDone = [storage](DoneWith result) { + if (result == DoneWith::Success) { + // Avoid downloading assets that are present in unzipped tree + InternalStorage &storageData = *storage; + storageData.assets.assetsToDownload = + filterDownloadableAssets(storageData.assets.allAssets, storageData.tempDir); + } else { + qWarning() << "ZipFile failed"; + } + return DoneResult::Success; // Ignore unzip failure + }; + + const LoopUntil downloadIterator([storage](int iteration) { + return iteration < storage->assets.assetsToDownload.count(); + }); + + const Storage assetStorage; + + const auto onAssetsDownloadGroupSetup = [this, storage] { + d->setProgress(0, storage->assets.assetsToDownload.size(), tr("Downloading assets...")); + }; + + const auto onAssetDownloadSetup = [this, storage, downloadIterator](NetworkQuery &query) { + query.setNetworkAccessManager(d->m_manager); + query.setRequest(QNetworkRequest(storage->assets.remoteUrl.resolved( + storage->assets.assetsToDownload.at(downloadIterator.iteration())))); + }; + const auto onAssetDownloadDone = [assetStorage](const NetworkQuery &query, DoneWith result) { + if (result == DoneWith::Success) + *assetStorage = query.reply()->readAll(); + }; + + const auto onAssetWriteSetup = [storage, downloadIterator, assetStorage]( + ConcurrentCall &async) { + const QString filePath = storage->tempDir.absoluteFilePath( + storage->assets.assetsToDownload.at(downloadIterator.iteration()).toString()); + async.setConcurrentCallData(writeAsset, *assetStorage, filePath); + }; + const auto onAssetWriteDone = [this, storage](DoneWith result) { + if (result != DoneWith::Success) { + qWarning() << "Asset write failed"; + return; + } + InternalStorage &storageData = *storage; + ++storageData.doneCount; + d->updateProgress(storageData.doneCount, storageData.assets.assetsToDownload.size()); + }; + + const LoopUntil copyIterator([storage](int iteration) { + return iteration < storage->assets.allAssets.count(); + }); + + const auto onAssetsCopyGroupSetup = [this, storage] { + storage->doneCount = 0; + d->setProgress(0, storage->assets.allAssets.size(), tr("Copying assets...")); + }; + + const auto onAssetCopySetup = [storage, copyIterator](ConcurrentCall &async) { + const QString fileName = storage->assets.allAssets.at(copyIterator.iteration()).toString(); + const QString sourcePath = storage->tempDir.absoluteFilePath(fileName); + const QString destPath = storage->baseLocalDir.absoluteFilePath(fileName); + async.setConcurrentCallData(copyAndCheck, sourcePath, destPath); + }; + const auto onAssetCopyDone = [this, storage] (DoneWith result) { + if (result != DoneWith::Success) { + qWarning() << "Asset copy failed"; + return; + } + InternalStorage &storageData = *storage; + ++storageData.doneCount; + d->updateProgress(storageData.doneCount, storageData.assets.allAssets.size()); + }; + + const Group root { + storage, + onGroupSetup(onSetup), + NetworkQueryTask(onJsonDownloadSetup, onJsonDownloadDone), + ConcurrentCallTask(onReadAssetsFileSetup, onReadAssetsFileDone, CallDoneIf::Success), + Group { + onGroupSetup(onSkipIfAllAssetsPresent), + NetworkQueryTask(onZipDownloadSetup, onZipDownloadDone), + ConcurrentCallTask(onUnzipSetup, onUnzipDone), + Group { + parallelIdealThreadCountLimit, + downloadIterator, + onGroupSetup(onAssetsDownloadGroupSetup), + Group { + assetStorage, + NetworkQueryTask(onAssetDownloadSetup, onAssetDownloadDone), + ConcurrentCallTask(onAssetWriteSetup, onAssetWriteDone) + } + }, + Group { + parallelIdealThreadCountLimit, + copyIterator, + onGroupSetup(onAssetsCopyGroupSetup), + ConcurrentCallTask(onAssetCopySetup, onAssetCopyDone) + } + } + }; + d->m_taskTreeRunner.start(root,[this](TaskTree *) { emit started(); }, + [this](DoneWith result) { emit finished(result == DoneWith::Success); }); +} diff --git a/tests/manual/tasking/assetdownloader/assetdownloader.h b/tests/manual/tasking/assetdownloader/assetdownloader.h new file mode 100644 index 00000000000..2b04d710f7e --- /dev/null +++ b/tests/manual/tasking/assetdownloader/assetdownloader.h @@ -0,0 +1,102 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef ASSETDOWNLOADER_H +#define ASSETDOWNLOADER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +#include + +QT_BEGIN_NAMESPACE +class QNetworkAccessManager; +QT_END_NAMESPACE + +class AssetDownloaderPrivate; + +class AssetDownloader : public QObject +{ + Q_OBJECT + + Q_PROPERTY( + QUrl downloadBase + READ downloadBase + WRITE setDownloadBase + NOTIFY downloadBaseChanged) + + Q_PROPERTY( + QUrl preferredLocalDownloadDir + READ preferredLocalDownloadDir + WRITE setPreferredLocalDownloadDir + NOTIFY preferredLocalDownloadDirChanged) + + Q_PROPERTY( + QUrl offlineAssetsFilePath + READ offlineAssetsFilePath + WRITE setOfflineAssetsFilePath + NOTIFY offlineAssetsFilePathChanged) + + Q_PROPERTY( + QString jsonFileName + READ jsonFileName + WRITE setJsonFileName + NOTIFY jsonFileNameChanged) + + Q_PROPERTY( + QString zipFileName + READ zipFileName + WRITE setZipFileName + NOTIFY zipFileNameChanged) + +public: + AssetDownloader(QObject *parent = nullptr); + ~AssetDownloader(); + + void setNetworkAccessManager(QNetworkAccessManager *manager); + + QUrl downloadBase() const; + void setDownloadBase(const QUrl &downloadBase); + + QUrl preferredLocalDownloadDir() const; + void setPreferredLocalDownloadDir(const QUrl &localDir); + + QUrl offlineAssetsFilePath() const; + void setOfflineAssetsFilePath(const QUrl &offlineAssetsFilePath); + + QString jsonFileName() const; + void setJsonFileName(const QString &jsonFileName); + + QString zipFileName() const; + void setZipFileName(const QString &zipFileName); + +public Q_SLOTS: + void start(); + +Q_SIGNALS: + void started(); + void finished(bool success); + void progressChanged(int progressValue, int progressMaximum, const QString &progressText); + + void downloadBaseChanged(const QUrl &); + void preferredLocalDownloadDirChanged(const QUrl &url); + void offlineAssetsFilePathChanged(const QUrl &); + void jsonFileNameChanged(const QString &); + void zipFileNameChanged(const QString &); + +private: + std::unique_ptr d; +}; + +#endif // ASSETDOWNLOADER_H diff --git a/tests/manual/tasking/assetdownloader/assetdownloader.qbs b/tests/manual/tasking/assetdownloader/assetdownloader.qbs new file mode 100644 index 00000000000..854e2eed6ba --- /dev/null +++ b/tests/manual/tasking/assetdownloader/assetdownloader.qbs @@ -0,0 +1,13 @@ +QtcManualTest { + name: "Tasking assetdownloader" + type: ["application"] + + Depends { name: "Qt"; submodules: ["concurrent", "network", "widgets", "core-private", "gui-private"] } + Depends { name: "Tasking" } + + files: [ + "assetdownloader.cpp", + "assetdownloader.h", + "main.cpp", + ] +} diff --git a/tests/manual/tasking/assetdownloader/main.cpp b/tests/manual/tasking/assetdownloader/main.cpp new file mode 100644 index 00000000000..58cc9f1ac29 --- /dev/null +++ b/tests/manual/tasking/assetdownloader/main.cpp @@ -0,0 +1,50 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "assetdownloader.h" + +#include +#include +#include +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + app.setOrganizationName("QtProject"); + app.setApplicationName("Asset Downloader"); + + QProgressDialog progress; + progress.setAutoClose(false); + progress.setRange(0, 0); + QObject::connect(&progress, &QProgressDialog::canceled, &app, &QApplication::quit); + + QNetworkAccessManager manager; + + AssetDownloader downloader; + downloader.setNetworkAccessManager(&manager); + downloader.setJsonFileName("car-configurator-assets-v1.json"); + downloader.setZipFileName("car-configurator-assets-v1.zip"); + downloader.setDownloadBase(QUrl("https://download.qt.io/learning/examples/")); + + QObject::connect(&downloader, &AssetDownloader::started, + &progress, &QProgressDialog::show); + QObject::connect(&downloader, &AssetDownloader::progressChanged, &progress, + [&](int progressValue, int progressMaximum, const QString &progressText) { + progress.setLabelText(progressText); + progress.setMaximum(progressMaximum); + progress.setValue(progressValue); + }); + QObject::connect(&downloader, &AssetDownloader::finished, &progress, [&](bool success) { + progress.reset(); + progress.hide(); + if (success) + QMessageBox::information(nullptr, "Asset Downloader", "Download Finished Successfully."); + else + QMessageBox::warning(nullptr, "Asset Downloader", "Download Finished with an Error."); + }); + + downloader.start(); + + return app.exec(); +}