diff --git a/CMakeLists.txt b/CMakeLists.txt index 650c2b4..3ffa92a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,14 +72,14 @@ endif() include(OB/FetchQx) ob_fetch_qx( - REF "v0.5.5.1" + REF "7acb9d0f3e65de2f0a6e91c979b1216e6be7de7d" COMPONENTS ${CLIFP_QX_COMPONENTS} ) # Fetch libfp (build and import from source) include(OB/Fetchlibfp) -ob_fetch_libfp("v0.5.1.1") +ob_fetch_libfp("d27276305defe2fbd98946b23ec9d135ce3ae1bd") # Fetch QI-QMP (build and import from source) include(OB/FetchQI-QMP) diff --git a/README.md b/README.md index 74de0b5..eb67ab7 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,14 @@ The **-title-strict** and **-subtitle-strict** options only consider exact match Tip: You can use **-subtitle** with an empty string (i.e. `-s ""`) to see all of the additional-apps for a given title. ### Command List: + +**download** - Downloads data packs for games that require them in bulk + +Options: +- **-p | --playlist** Name of the playlist to download games for. + +-------------------------------------------------------------------------------- + **link** - Creates a shortcut to a Flashpoint title Options: diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index af6d3df..f91780d 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -11,6 +11,8 @@ set(CLIFP_SOURCE kernel/errorstatus.cpp command/command.h command/command.cpp + command/c-download.h + command/c-download.cpp command/c-link.h command/c-link.cpp command/c-play.h diff --git a/app/src/command/c-download.cpp b/app/src/command/c-download.cpp new file mode 100644 index 0000000..9ecf18c --- /dev/null +++ b/app/src/command/c-download.cpp @@ -0,0 +1,114 @@ +// Unit Include +#include "c-download.h" + +// Project Includes +#include "task/t-download.h" +#include "task/t-generic.h" + +//=============================================================================================================== +// CDownloadError +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Private: +CDownloadError::CDownloadError(Type t, const QString& s) : + mType(t), + mSpecific(s) +{} + +//-Instance Functions------------------------------------------------------------- +//Public: +bool CDownloadError::isValid() const { return mType != NoError; } +QString CDownloadError::specific() const { return mSpecific; } +CDownloadError::Type CDownloadError::type() const { return mType; } + +//Private: +Qx::Severity CDownloadError::deriveSeverity() const { return Qx::Critical; } +quint32 CDownloadError::deriveValue() const { return mType; } +QString CDownloadError::derivePrimary() const { return ERR_STRINGS.value(mType); } +QString CDownloadError::deriveSecondary() const { return mSpecific; } + +//=============================================================================================================== +// CDownload +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Public: +CDownload::CDownload(Core& coreRef) : Command(coreRef) {} + +//-Instance Functions------------------------------------------------------------- +//Protected: +QList CDownload::options() { return CL_OPTIONS_SPECIFIC + Command::options(); } +QSet CDownload::requiredOptions() { return CL_OPTIONS_REQUIRED + Command::requiredOptions(); } +QString CDownload::name() { return NAME; } + +Qx::Error CDownload::perform() +{ + QString playlistName = mParser.value(CL_OPTION_PLAYLIST).trimmed(); + mCore.setStatus(STATUS_DOWNLOAD, playlistName); + + Fp::Db* db = mCore.fpInstall().database(); + Fp::PlaylistManager* pm = mCore.fpInstall().playlistManager(); + if(Qx::Error pError = pm->populate(); pError.isValid()) + return pError; + + // Find playlist + QList playlists = pm->playlists(); + auto pItr = std::find_if(playlists.cbegin(), playlists.cend(), [&playlistName](auto p){ + return p.title() == playlistName || p.title().trimmed() == playlistName; // Some playlists have spaces for sorting purposes + }); + + if(pItr == playlists.cend()) + { + CDownloadError err(CDownloadError::InvalidPlaylist, playlistName); + mCore.postError(NAME, err); + return err; + } + mCore.logEvent(NAME, LOG_PLAYLIST_MATCH.arg(pItr->id().toString(QUuid::WithoutBraces))); + + // Queue downloads for each game + TDownload* downloadTask = new TDownload(&mCore); + downloadTask->setStage(Task::Stage::Primary); + downloadTask->setDescription(u"playlist data packs"_s); + QList dataIds; + + for(const auto& pg : pItr->playlistGames()) + { + // Get data + Fp::GameData gameData; + if(Fp::DbError gdErr = db->getGameData(gameData, pg.gameId()); gdErr.isValid()) + { + mCore.postError(NAME, gdErr); + return gdErr; + } + + if(gameData.isNull()) + { + mCore.logEvent(NAME, LOG_NON_DATAPACK.arg(pg.gameId().toString(QUuid::WithoutBraces))); + continue; + } + + // Queue download + QString filename = gameData.path(); + downloadTask->addFile({.target = mCore.datapackUrl(filename), .dest = mCore.datapackPath(filename), .checksum = gameData.sha256()}); + + // Note data id + dataIds.append(gameData.id()); + } + + // Enqueue download task + mCore.enqueueSingleTask(downloadTask); + + // Enqueue onDiskState update task + Core* corePtr = &mCore; // Safe, will outlive task + TGeneric* onDiskUpdateTask = new TGeneric(corePtr); + onDiskUpdateTask->setStage(Task::Stage::Primary); + onDiskUpdateTask->setDescription(u"Update GameData onDisk state."_s); + onDiskUpdateTask->setAction([dataIds, corePtr]{ + return corePtr->fpInstall().database()->updateGameDataOnDiskState(dataIds, true); + }); + mCore.enqueueSingleTask(onDiskUpdateTask); + + // Return success + return CDownloadError(); +} diff --git a/app/src/command/c-download.h b/app/src/command/c-download.h new file mode 100644 index 0000000..1302a1c --- /dev/null +++ b/app/src/command/c-download.h @@ -0,0 +1,89 @@ +#ifndef CDOWNLOAD_H +#define CDOWNLOAD_H + +// Qx Includes +#include + +// Project Includes +#include "command/command.h" + +class QX_ERROR_TYPE(CDownloadError, "CDownloadError", 1217) +{ + friend class CDownload; + //-Class Enums------------------------------------------------------------- +public: + enum Type + { + NoError, + InvalidPlaylist + }; + + //-Class Variables------------------------------------------------------------- +private: + static inline const QHash ERR_STRINGS{ + {NoError, u""_s}, + {InvalidPlaylist, u""_s} + }; + + //-Instance Variables------------------------------------------------------------- +private: + Type mType; + QString mSpecific; + + //-Constructor------------------------------------------------------------- +private: + CDownloadError(Type t = NoError, const QString& s = {}); + + //-Instance Functions------------------------------------------------------------- +public: + bool isValid() const; + Type type() const; + QString specific() const; + +private: + Qx::Severity deriveSeverity() const override; + quint32 deriveValue() const override; + QString derivePrimary() const override; + QString deriveSecondary() const override; +}; + +class CDownload : public Command +{ + //-Class Variables------------------------------------------------------------------------------------------------------ +private: + // Status + static inline const QString STATUS_DOWNLOAD = u"Downloading data packs"_s; + + // Logging + static inline const QString LOG_PLAYLIST_MATCH = u"Playlist matches ID: %1"_s; + static inline const QString LOG_NON_DATAPACK = u"Game %1 does not use a data pack."_s; + + // Command line option strings + static inline const QString CL_OPT_PLAYLIST_S_NAME = u"p"_s; + static inline const QString CL_OPT_PLAYLIST_L_NAME = u"playlist"_s; + static inline const QString CL_OPT_PLAYLIST_DESC = u"Name of the playlist to download games for."_s; + + // Command line options + static inline const QCommandLineOption CL_OPTION_PLAYLIST{{CL_OPT_PLAYLIST_S_NAME, CL_OPT_PLAYLIST_L_NAME}, CL_OPT_PLAYLIST_DESC, u"playlist"_s}; // Takes value + static inline const QList CL_OPTIONS_SPECIFIC{&CL_OPTION_PLAYLIST}; + static inline const QSet CL_OPTIONS_REQUIRED{&CL_OPTION_PLAYLIST}; + +public: + // Meta + static inline const QString NAME = u"download"_s; + static inline const QString DESCRIPTION = u"Download game data packs in bulk"_s; + + //-Constructor---------------------------------------------------------------------------------------------------------- +public: + CDownload(Core& coreRef); + + //-Instance Functions------------------------------------------------------------------------------------------------------ +protected: + QList options() override; + QSet requiredOptions() override; + QString name() override; + Qx::Error perform() override; +}; +REGISTER_COMMAND(CDownload::NAME, CDownload, CDownload::DESCRIPTION); + +#endif // CDOWNLOAD_H diff --git a/app/src/command/c-prepare.cpp b/app/src/command/c-prepare.cpp index fae8ef5..22df190 100644 --- a/app/src/command/c-prepare.cpp +++ b/app/src/command/c-prepare.cpp @@ -14,7 +14,7 @@ CPrepare::CPrepare(Core& coreRef) : TitleCommand(coreRef) {} //-Instance Functions------------------------------------------------------------- //Protected: -QList CPrepare::options() { return {&CL_OPTION_ID, &CL_OPTION_TITLE, &CL_OPTION_TITLE_STRICT}; } +QList CPrepare::options() { return TitleCommand::options(); } QString CPrepare::name() { return NAME; } Qx::Error CPrepare::perform() diff --git a/app/src/command/c-update.cpp b/app/src/command/c-update.cpp index 8ed4f8f..b2d131a 100644 --- a/app/src/command/c-update.cpp +++ b/app/src/command/c-update.cpp @@ -330,9 +330,8 @@ CUpdateError CUpdate::checkAndPrepareUpdate() const QString tempName = u"clifp_update.zip"_s; TDownload* downloadTask = new TDownload(&mCore); downloadTask->setStage(Task::Stage::Primary); - downloadTask->setTargetFile(aItr->browser_download_url); - downloadTask->setDestinationPath(uDownloadDir.absolutePath()); - downloadTask->setDestinationFilename(tempName); + downloadTask->setDescription(u"update"_s); + downloadTask->addFile({.target = aItr->browser_download_url, .dest = uDownloadDir.absoluteFilePath(tempName)}); mCore.enqueueSingleTask(downloadTask); TExtract* extractTask = new TExtract(&mCore); diff --git a/app/src/kernel/core.cpp b/app/src/kernel/core.cpp index 4f7c9b2..00f451d 100644 --- a/app/src/kernel/core.cpp +++ b/app/src/kernel/core.cpp @@ -507,6 +507,56 @@ Qx::Error Core::findAddAppIdFromName(QUuid& returnBuffer, QUuid parent, QString return searchAndFilterEntity(returnBuffer, name, exactName, parent); } +QString Core::datapackPath(const QString& packFilename) const +{ + static QDir folder(mFlashpointInstall->fullPath() + '/' + mFlashpointInstall->preferences().dataPacksFolderPath); + return folder.absoluteFilePath(packFilename); +} + +QUrl Core::datapackUrl(const QString& packFilename) const +{ + static QString urlBase = [&packFilename, this]{ + /* TODO: This is ugly, it would be ideal to handle this more gracefully as a regular error as it once was, but + * this function design doesn't really work with that and for now this source can be relied upon. + */ + auto sources = mFlashpointInstall->preferences().gameDataSources; + if(!sources.contains(u"Flashpoint Project"_s)) + qCritical("Expected game data source 'Flashpoint Project' missing!"); + + return sources.value(u"Flashpoint Project"_s).arguments.value(0); + }(); + + return urlBase + '/' + packFilename; +} + +bool Core::datapackIsPresent(const Fp::GameData& gameData) +{ + // Get current file checksum if it exists + QString packFilename = gameData.path(); + QFile packFile(datapackPath(packFilename)); + bool checksumMatches = false; + + if(!gameData.presentOnDisk() || !packFile.exists()) + return false; + + // Checking the sum in addition to the flag is somewhat overkill, but may help in situations + // where the flag is set but the datapack's contents have changed + Qx::IoOpReport checksumReport = Qx::fileMatchesChecksum(checksumMatches, packFile, gameData.sha256(), QCryptographicHash::Sha256); + if(checksumReport.isFailure()) + { + logError(NAME, checksumReport); + return false; + } + + if(!checksumMatches) + { + postError(NAME, CoreError(CoreError::DataPackSumMismatch, packFilename, Qx::Warning)); + return false; + } + + return true; +} + bool Core::blockNewInstances() { bool b = Qx::enforceSingleInstance(SINGLE_INSTANCE_ID); @@ -697,51 +747,20 @@ Qx::Error Core::enqueueDataPackTasks(const Fp::GameData& gameData) { logEvent(NAME, LOG_EVENT_ENQ_DATA_PACK); - // Extract relevant data - QString packDestFolderPath = mFlashpointInstall->fullPath() + '/' + mFlashpointInstall->preferences().dataPacksFolderPath; - QString packFileName = gameData.path(); - QString packSha256 = gameData.sha256(); - QString packParameters = gameData.parameters(); - QFile packFile(packDestFolderPath + '/' + packFileName); + QString packFilename = gameData.path(); + QString packPath = datapackPath(packFilename); - // Get current file checksum if it exists - bool checksumMatches = false; - - if(gameData.presentOnDisk() && packFile.exists()) + // Enqueue pack download if it's not available + if(!datapackIsPresent(gameData)) { - // Checking the sum in addition to the flag is somewhat overkill, but may help in situations - // where the flag is set but the datapack's contents have changed - Qx::IoOpReport checksumReport = Qx::fileMatchesChecksum(checksumMatches, packFile, packSha256, QCryptographicHash::Sha256); - if(checksumReport.isFailure()) - logError(NAME, checksumReport); - - if(checksumMatches) - logEvent(NAME, LOG_EVENT_DATA_PACK_FOUND); - else - postError(NAME, CoreError(CoreError::DataPackSumMismatch, packFileName, Qx::Warning)); - } - else logEvent(NAME, LOG_EVENT_DATA_PACK_MISS); - // Enqueue pack download if it doesn't exist or is different than expected - if(!packFile.exists() || !checksumMatches) - { - if(!mFlashpointInstall->preferences().gameDataSources.contains(u"Flashpoint Project"_s)) - { - CoreError err(CoreError::DataPackSourceMissing, u"Flashpoint Project"_s); - postError(NAME, err); - return err; - } - - Fp::GameDataSource gameSource = mFlashpointInstall->preferences().gameDataSources.value(u"Flashpoint Project"_s); - QString gameSourceBase = gameSource.arguments.value(0); + QUrl packUrl = datapackUrl(packFilename); TDownload* downloadTask = new TDownload(this); downloadTask->setStage(Task::Stage::Auxiliary); - downloadTask->setDestinationPath(packDestFolderPath); - downloadTask->setDestinationFilename(packFileName); - downloadTask->setTargetFile(gameSourceBase + '/' + packFileName); - downloadTask->setSha256(packSha256); + downloadTask->setDescription(u"data pack "_s + packFilename); + downloadTask->addFile({.target = packUrl, .dest = packPath, .checksum = gameData.sha256()}); mTaskQueue.push(downloadTask); logTask(NAME, downloadTask); @@ -753,21 +772,24 @@ Qx::Error Core::enqueueDataPackTasks(const Fp::GameData& gameData) onDiskUpdateTask->setStage(Task::Stage::Auxiliary); onDiskUpdateTask->setDescription(u"Update GameData onDisk state."_s); onDiskUpdateTask->setAction([gameDataId, this]{ - return mFlashpointInstall->database()->updateGameDataOnDiskState(gameDataId, true); + return mFlashpointInstall->database()->updateGameDataOnDiskState({gameDataId}, true); }); mTaskQueue.push(onDiskUpdateTask); logTask(NAME, onDiskUpdateTask); } + else + logEvent(NAME, LOG_EVENT_DATA_PACK_FOUND); // Enqueue pack mount or extract - if(packParameters.contains(u"-extract"_s)) + + if(gameData.parameters().contains(u"-extract"_s)) { logEvent(NAME, LOG_EVENT_DATA_PACK_NEEDS_EXTRACT); TExtract* extractTask = new TExtract(this); extractTask->setStage(Task::Stage::Auxiliary); - extractTask->setPackPath(packDestFolderPath + '/' + packFileName); + extractTask->setPackPath(packPath); extractTask->setPathInPack(u"content"_s); extractTask->setDestinationPath(mFlashpointInstall->preferences().htdocsFolderPath); @@ -782,7 +804,7 @@ Qx::Error Core::enqueueDataPackTasks(const Fp::GameData& gameData) TMount* mountTask = new TMount(this); mountTask->setStage(Task::Stage::Auxiliary); mountTask->setTitleId(gameData.gameId()); - mountTask->setPath(packDestFolderPath + '/' + packFileName); + mountTask->setPath(packPath); mountTask->setDaemon(mFlashpointInstall->outfittedDaemon()); mTaskQueue.push(mountTask); diff --git a/app/src/kernel/core.h b/app/src/kernel/core.h index f134043..1a8edb5 100644 --- a/app/src/kernel/core.h +++ b/app/src/kernel/core.h @@ -289,10 +289,13 @@ class Core : public QObject Qx::Error initialize(QStringList& commandLine); void attachFlashpoint(std::unique_ptr flashpointInstall); - // Helper + // Helper (TODO: Move some of these, especially the lower ones, to libfp) QString resolveTrueAppPath(const QString& appPath, const QString& platform); Qx::Error findGameIdFromTitle(QUuid& returnBuffer, QString title, bool exactTitle = true); Qx::Error findAddAppIdFromName(QUuid& returnBuffer, QUuid parent, QString name, bool exactName = true); + QString datapackPath(const QString& packFilename) const; + QUrl datapackUrl(const QString& packFilename) const; + bool datapackIsPresent(const Fp::GameData& gameData); // Common bool blockNewInstances(); diff --git a/app/src/task/t-download.cpp b/app/src/task/t-download.cpp index 3fadb9b..a5087ae 100644 --- a/app/src/task/t-download.cpp +++ b/app/src/task/t-download.cpp @@ -35,9 +35,7 @@ TDownload::TDownload(QObject* parent) : { // Setup download manager mDownloadManager.setOverwrite(true); - - // Since this is only for one download, the size will be adjusted to the correct total as soon as the download starts - mDownloadManager.setSkipEnumeration(true); + mDownloadManager.setVerificationMethod(QCryptographicHash::Sha256); // Download event handlers connect(&mDownloadManager, &Qx::AsyncDownloadManager::sslErrors, this, [this](Qx::Error errorMsg, bool* ignore) { @@ -69,36 +67,37 @@ QString TDownload::name() const { return NAME; } QStringList TDownload::members() const { QStringList ml = Task::members(); - ml.append(u".destinationPath() = \""_s + QDir::toNativeSeparators(mDestinationPath) + u"\""_s); - ml.append(u".destinationFilename() = \""_s + mDestinationFilename + u"\""_s); - ml.append(u".targetFile() = \""_s + mTargetFile.toString() + u"\""_s); - ml.append(u".sha256() = "_s + mSha256); + + QString files = u".files() = {\n"_s; + for(auto i = 0; i < 10 && i < mFiles.size(); i++) + { + auto f = mFiles.at(i); + bool sum = !f.checksum.isEmpty(); + files += u"\t"_s + FILE_DOWNLOAD_TEMPLATE.arg(f.target.toString(), f.dest, sum ? f.checksum : FILE_NO_CHECKSUM) + '\n'; + } + if(mFiles.size() > 10) + files += FILE_DOWNLOAD_ELIDE.arg(mFiles.size() - 10) + '\n'; + files += u"}"_s; + ml.append(files); + ml.append(u".description() = \""_s + mDescription + u"\""_s); + return ml; } -QString TDownload::destinationPath() const { return mDestinationPath; } -QString TDownload::destinationFilename() const { return mDestinationFilename; } -QUrl TDownload::targetFile() const { return mTargetFile; } -QString TDownload::sha256() const { return mSha256; } +QList TDownload::files() const { return mFiles; } +QString TDownload::description() const { return mDescription; } -void TDownload::setDestinationPath(QString path) { mDestinationPath = path; } -void TDownload::setDestinationFilename(QString filename) { mDestinationFilename = filename; } -void TDownload::setTargetFile(QUrl targetFile) { mTargetFile = targetFile; } -void TDownload::setSha256(QString sha256) { mSha256 = sha256; } +void TDownload::addFile(const Qx::DownloadTask file) { mFiles.append(file); } +void TDownload::setDescription(const QString& desc) { mDescription = desc; } void TDownload::perform() { - // Setup download - QFile file(mDestinationPath + '/' + mDestinationFilename); - QFileInfo fileInfo(file); - Qx::DownloadTask download{ - .target = mTargetFile, - .dest = fileInfo.absoluteFilePath() - }; - mDownloadManager.appendTask(download); + // Add files + for(const auto& f : mFiles) + mDownloadManager.appendTask(f); // Log/label string - QString label = LOG_EVENT_DOWNLOADING_FILE.arg(fileInfo.fileName()); + QString label = LOG_EVENT_DOWNLOAD.arg(mDescription); emit eventOccurred(NAME, label); // Start download @@ -123,28 +122,11 @@ void TDownload::postDownload(Qx::DownloadManagerReport downloadReport) // Handle result emit longTaskFinished(); - if(downloadReport.wasSuccessful()) - { - // Confirm checksum is correct, if supplied - if(!mSha256.isEmpty()) - { - QFile file(mDestinationPath + '/' + mDestinationFilename); - bool checksumMatch; - Qx::IoOpReport cr = Qx::fileMatchesChecksum(checksumMatch, file, mSha256, QCryptographicHash::Sha256); - if(cr.isFailure() || !checksumMatch) - { - TDownloadError err(TDownloadError::ChecksumMismatch, cr.isFailure() ? cr.outcomeInfo() : u""_s); - errorStatus = err; - emit errorOccurred(NAME, errorStatus); - } - } - + if(downloadReport.wasSuccessful()) emit eventOccurred(NAME, LOG_EVENT_DOWNLOAD_SUCC); - } else { - TDownloadError err(TDownloadError::Incomeplete, downloadReport.outcomeString()); - errorStatus = err; + errorStatus = TDownloadError(TDownloadError::Incomeplete, downloadReport.outcomeString()); emit errorOccurred(NAME, errorStatus); } diff --git a/app/src/task/t-download.h b/app/src/task/t-download.h index ff40bb9..867b6bb 100644 --- a/app/src/task/t-download.h +++ b/app/src/task/t-download.h @@ -11,33 +11,31 @@ class QX_ERROR_TYPE(TDownloadError, "TDownloadError", 1252) { friend class TDownload; - //-Class Enums------------------------------------------------------------- +//-Class Enums------------------------------------------------------------- public: enum Type { NoError = 0, - ChecksumMismatch = 1, Incomeplete = 2 }; - //-Class Variables------------------------------------------------------------- +//-Class Variables------------------------------------------------------------- private: static inline const QHash ERR_STRINGS{ {NoError, u""_s}, - {ChecksumMismatch, u"The file's checksum does not match its record!"_s}, - {Incomeplete, u"The download could not be completed."_s} + {Incomeplete, u"The download(s) could not be completed."_s} }; - //-Instance Variables------------------------------------------------------------- +//-Instance Variables------------------------------------------------------------- private: Type mType; QString mSpecific; - //-Constructor------------------------------------------------------------- +//-Constructor------------------------------------------------------------- private: TDownloadError(Type t = NoError, const QString& s = {}); - //-Instance Functions------------------------------------------------------------- +//-Instance Functions------------------------------------------------------------- public: bool isValid() const; Type type() const; @@ -59,25 +57,24 @@ class TDownload : public Task static inline const QString NAME = u"TDownload"_s; // Logging - static inline const QString LOG_EVENT_DOWNLOADING_FILE = u"Downloading file %1"_s; - static inline const QString LOG_EVENT_DOWNLOAD_SUCC = u"File downloaded successfully"_s; + static inline const QString LOG_EVENT_DOWNLOAD = u"Downloading %1"_s; + static inline const QString LOG_EVENT_DOWNLOAD_SUCC = u"File(s) downloaded successfully"_s; static inline const QString LOG_EVENT_DOWNLOAD_AUTH = u"File download unexpectedly requires authentication (%1)"_s; static inline const QString LOG_EVENT_STOPPING_DOWNLOADS = u"Stopping current download(s)..."_s; + // Members + static inline const QString FILE_NO_CHECKSUM = u"NO SUM"_s; + static inline const QString FILE_DOWNLOAD_TEMPLATE = uR"("%1" -> "%2" (%3))"_s; + static inline const QString FILE_DOWNLOAD_ELIDE = u"+%1 more..."_s; + //-Instance Variables------------------------------------------------------------------------------------------------ private: // Functional Qx::AsyncDownloadManager mDownloadManager; - /* NOTE: If it ever becomes required to perform multiple downloads in a run the DM instance should - * be made a static member of TDownload, or all downloads need to be determined at the same time - * and the task made capable of holding all of them - */ // Data - QString mDestinationPath; - QString mDestinationFilename; - QUrl mTargetFile; - QString mSha256; + QList mFiles; + QString mDescription; //-Constructor---------------------------------------------------------------------------------------------------------- public: @@ -88,15 +85,11 @@ class TDownload : public Task QString name() const override; QStringList members() const override; - QString destinationPath() const; - QString destinationFilename() const; - QUrl targetFile() const; - QString sha256() const; + QList files() const; + QString description() const; - void setDestinationPath(QString path); - void setDestinationFilename(QString filename); - void setTargetFile(QUrl targetFile); - void setSha256(QString sha256); + void addFile(const Qx::DownloadTask file); + void setDescription(const QString& desc); void perform() override; void stop() override;