From 4b522f40e88e1350434aaa7d77fc60cb7ca36930 Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Wed, 30 Jan 2019 20:32:48 -0600 Subject: Make logs more consistent in format and content --- src/modinfo.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/modinfo.cpp') diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 1c6139f6..7ae41b02 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -110,7 +110,7 @@ QString ModInfo::getContentTypeName(int contentType) case CONTENT_INI: return tr("INI files"); case CONTENT_MODGROUP: return tr("ModGroup files"); - default: throw MyException(tr("invalid content type %1").arg(contentType)); + default: throw MyException(tr("invalid content type: %1").arg(contentType)); } } @@ -133,7 +133,7 @@ ModInfo::Ptr ModInfo::getByIndex(unsigned int index) QMutexLocker locker(&s_Mutex); if (index >= s_Collection.size() && index != ULONG_MAX) { - throw MyException(tr("invalid mod index %1").arg(index)); + throw MyException(tr("invalid mod index: %1").arg(index)); } if (index == ULONG_MAX) return s_Collection[ModInfo::getIndex("Overwrite")]; return s_Collection[index]; -- cgit v1.3.1 From 2201a5b0dd739f273f2a906eda3d4d31335c7c71 Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Thu, 31 Jan 2019 11:31:22 -0600 Subject: Make gameShortName comparisons case insensitive --- src/modinfo.cpp | 16 ++++++++++++---- src/modlist.cpp | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) (limited to 'src/modinfo.cpp') diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 7ae41b02..905341b0 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -144,14 +144,22 @@ std::vector ModInfo::getByModID(QString game, int modID) { QMutexLocker locker(&s_Mutex); - auto iter = s_ModsByModID.find(std::pair(game, modID)); - if (iter == s_ModsByModID.end()) { + std::vector match; + for (auto iter : s_ModsByModID) { + if (iter.first.second == modID) { + if (iter.first.first.compare(game, Qt::CaseInsensitive) == 0) { + match = iter.second; + break; + } + } + } + if (match.empty()) { return std::vector(); } std::vector result; - for (auto idxIter = iter->second.begin(); idxIter != iter->second.end(); ++idxIter) { - result.push_back(getByIndex(*idxIter)); + for (auto iter : match) { + result.push_back(getByIndex(iter)); } return result; diff --git a/src/modlist.cpp b/src/modlist.cpp index 62c186a4..daa6ce73 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -253,7 +253,7 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const } else if (column == COL_GAME) { if (m_PluginContainer != nullptr) { for (auto game : m_PluginContainer->plugins()) { - if (game->gameShortName() == modInfo->getGameName()) + if (game->gameShortName().compare(modInfo->getGameName(), Qt::CaseInsensitive) == 0) return game->gameName(); } } -- cgit v1.3.1 From 4bea346b26f2e34120a42fdf2f1c5485ab93f5c9 Mon Sep 17 00:00:00 2001 From: Silarn Date: Sun, 27 Jan 2019 17:08:42 -0600 Subject: Reworking update checks to use the file update info with a fallback --- src/downloadmanager.cpp | 3673 +++++++++++++++++++++++------------------------ src/downloadmanager.h | 1132 ++++++++------- src/mainwindow.cpp | 147 +- src/mainwindow.h | 5 +- src/modinfo.cpp | 43 +- src/modinfobackup.h | 1 + src/modinfodialog.cpp | 3154 ++++++++++++++++++++-------------------- src/modinfoforeign.h | 1 + src/modinfooverwrite.h | 1 + src/modinforegular.cpp | 14 +- src/modinforegular.h | 2 +- src/modinfoseparator.h | 18 +- src/nexusinterface.cpp | 1444 +++++++++---------- src/nexusinterface.h | 847 ++++++----- src/organizer_en.ts | 426 +++--- 15 files changed, 5455 insertions(+), 5453 deletions(-) (limited to 'src/modinfo.cpp') diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index c927695e..8f36b9cf 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -1,1842 +1,1831 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "downloadmanager.h" - -#include "nxmurl.h" -#include "nexusinterface.h" -#include "nxmaccessmanager.h" -#include "iplugingame.h" -#include "downloadmanager.h" -#include -#include -#include "utility.h" -#include "selectiondialog.h" -#include "bbcode.h" -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - - -using namespace MOBase; - - -// TODO limit number of downloads, also display download during nxm requests, store modid/fileid with downloads - - -static const char UNFINISHED[] = ".unfinished"; - -unsigned int DownloadManager::DownloadInfo::s_NextDownloadID = 1U; -int DownloadManager::m_DirWatcherDisabler = 0; - - -DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createNew(const ModRepositoryFileInfo *fileInfo, const QStringList &URLs) -{ - DownloadInfo *info = new DownloadInfo; - info->m_DownloadID = s_NextDownloadID++; - info->m_StartTime.start(); - info->m_PreResumeSize = 0LL; - info->m_Progress = std::make_pair(0, "0.0 B/s "); - info->m_ResumePos = 0; - info->m_FileInfo = new ModRepositoryFileInfo(*fileInfo); - info->m_Urls = URLs; - info->m_CurrentUrl = 0; - info->m_Tries = AUTOMATIC_RETRIES; - info->m_State = STATE_STARTED; - info->m_TaskProgressId = TaskProgressManager::instance().getId(); - info->m_Reply = nullptr; - - return info; -} - -DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createFromMeta(const QString &filePath, bool showHidden, const QString outputDirectory) -{ - DownloadInfo *info = new DownloadInfo; - - QString metaFileName = filePath + ".meta"; - QFileInfo metaFileInfo(metaFileName); - if (QDir::fromNativeSeparators(metaFileInfo.path()).compare(QDir::fromNativeSeparators(outputDirectory), Qt::CaseInsensitive) != 0) return nullptr; - QSettings metaFile(metaFileName, QSettings::IniFormat); - if (!showHidden && metaFile.value("removed", false).toBool()) { - return nullptr; - } else { - info->m_Hidden = metaFile.value("removed", false).toBool(); - } - - QString fileName = QFileInfo(filePath).fileName(); - - if (fileName.endsWith(UNFINISHED)) { - info->m_FileName = fileName.mid( - 0, fileName.length() - static_cast(strlen(UNFINISHED))); - info->m_State = STATE_PAUSED; - } else { - info->m_FileName = fileName; - - if (metaFile.value("paused", false).toBool()) { - info->m_State = STATE_PAUSED; - } else if (metaFile.value("uninstalled", false).toBool()) { - info->m_State = STATE_UNINSTALLED; - } else if (metaFile.value("installed", false).toBool()) { - info->m_State = STATE_INSTALLED; - } else { - info->m_State = STATE_READY; - } - } - - info->m_DownloadID = s_NextDownloadID++; - info->m_Output.setFileName(filePath); - info->m_TotalSize = QFileInfo(filePath).size(); - info->m_PreResumeSize = info->m_TotalSize; - info->m_CurrentUrl = 0; - info->m_Urls = metaFile.value("url", "").toString().split(";"); - info->m_Tries = 0; - info->m_TaskProgressId = TaskProgressManager::instance().getId(); - QString gameName = metaFile.value("gameName", "").toString(); - int modID = metaFile.value("modID", 0).toInt(); - int fileID = metaFile.value("fileID", 0).toInt(); - info->m_FileInfo = new ModRepositoryFileInfo(gameName, modID, fileID); - info->m_FileInfo->name = metaFile.value("name", "").toString(); - if (info->m_FileInfo->name == "0") { - // bug in earlier version - info->m_FileInfo->name = ""; - } - info->m_FileInfo->modName = metaFile.value("modName", "").toString(); - info->m_FileInfo->gameName = gameName; - info->m_FileInfo->modID = modID; - info->m_FileInfo->fileID = fileID; - info->m_FileInfo->description = metaFile.value("description").toString(); - info->m_FileInfo->version.parse(metaFile.value("version", "0").toString()); - info->m_FileInfo->newestVersion.parse(metaFile.value("newestVersion", "0").toString()); - info->m_FileInfo->categoryID = metaFile.value("category", 0).toInt(); - info->m_FileInfo->fileCategory = metaFile.value("fileCategory", 0).toInt(); - info->m_FileInfo->repository = metaFile.value("repository", "Nexus").toString(); - info->m_FileInfo->userData = metaFile.value("userData").toMap(); - info->m_Reply = nullptr; - - return info; -} - -void DownloadManager::startDisableDirWatcher() -{ - DownloadManager::m_DirWatcherDisabler++; -} - - -void DownloadManager::endDisableDirWatcher() -{ - if (DownloadManager::m_DirWatcherDisabler > 0) - { - if (DownloadManager::m_DirWatcherDisabler == 1) - QCoreApplication::processEvents(); - DownloadManager::m_DirWatcherDisabler--; - } - else { - DownloadManager::m_DirWatcherDisabler = 0; - } -} - -void DownloadManager::DownloadInfo::setName(QString newName, bool renameFile) -{ - QString oldMetaFileName = QString("%1.meta").arg(m_FileName); - m_FileName = QFileInfo(newName).fileName(); - if ((m_State == DownloadManager::STATE_STARTED) || - (m_State == DownloadManager::STATE_DOWNLOADING) || - (m_State == DownloadManager::STATE_PAUSED)) { - newName.append(UNFINISHED); - oldMetaFileName = QString("%1%2.meta").arg(m_FileName).arg(UNFINISHED); - } - if (renameFile) { - if ((newName != m_Output.fileName()) && !m_Output.rename(newName)) { - reportError(tr("failed to rename \"%1\" to \"%2\"").arg(m_Output.fileName()).arg(newName)); - return; - } - - QFile metaFile(QFileInfo(newName).path() + "/" + oldMetaFileName); - if (metaFile.exists()) - metaFile.rename(newName.mid(0).append(".meta")); - } - if (!m_Output.isOpen()) { - // can't set file name if it's open - m_Output.setFileName(newName); - } -} - -bool DownloadManager::DownloadInfo::isPausedState() -{ - return m_State == STATE_PAUSED || m_State == STATE_ERROR; -} - -QString DownloadManager::DownloadInfo::currentURL() -{ - return m_Urls[m_CurrentUrl]; -} - - -DownloadManager::DownloadManager(NexusInterface *nexusInterface, QObject *parent) - : IDownloadManager(parent), m_NexusInterface(nexusInterface), m_DirWatcher(), m_ShowHidden(false), - m_DateExpression("/Date\\((\\d+)\\)/") -{ - connect(&m_DirWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); - m_TimeoutTimer.setSingleShot(false); - //connect(&m_TimeoutTimer, SIGNAL(timeout()), this, SLOT(checkDownloadTimeout())); - m_TimeoutTimer.start(5 * 1000); -} - - -DownloadManager::~DownloadManager() -{ - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { - delete *iter; - } - m_ActiveDownloads.clear(); -} - - -bool DownloadManager::downloadsInProgress() -{ - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { - if ((*iter)->m_State < STATE_READY) { - return true; - } - } - return false; -} - -bool DownloadManager::downloadsInProgressNoPause() -{ - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { - if ((*iter)->m_State < STATE_READY && (*iter)->m_State != STATE_PAUSED) { - return true; - } - } - return false; -} - - -void DownloadManager::pauseAll() -{ - - // first loop: pause all downloads - for (int i = 0; i < m_ActiveDownloads.count(); ++i) { - if (m_ActiveDownloads[i]->m_State < STATE_READY) { - pauseDownload(i); - } - } - - ::Sleep(100); - - bool done = false; - QTime startTime = QTime::currentTime(); - // further loops: busy waiting for all downloads to complete. This could be neater... - while (!done && (startTime.secsTo(QTime::currentTime()) < 5)) { - QCoreApplication::processEvents(); - done = true; - foreach (DownloadInfo *info, m_ActiveDownloads) { - if ((info->m_State < STATE_CANCELED) || - (info->m_State == STATE_FETCHINGFILEINFO) || - (info->m_State == STATE_FETCHINGMODINFO)) { - done = false; - break; - } - } - if (!done) { - ::Sleep(100); - } - } - -} - - -void DownloadManager::setOutputDirectory(const QString &outputDirectory) -{ - QStringList directories = m_DirWatcher.directories(); - if (directories.length() != 0) { - m_DirWatcher.removePaths(directories); - } - m_OutputDirectory = QDir::fromNativeSeparators(outputDirectory); - refreshList(); - m_DirWatcher.addPath(m_OutputDirectory); -} - - -void DownloadManager::setPreferredServers(const std::map &preferredServers) -{ - m_PreferredServers = preferredServers; -} - - -void DownloadManager::setSupportedExtensions(const QStringList &extensions) -{ - m_SupportedExtensions = extensions; - refreshList(); -} - -void DownloadManager::setShowHidden(bool showHidden) -{ - m_ShowHidden = showHidden; - refreshList(); -} - -void DownloadManager::setPluginContainer(PluginContainer *pluginContainer) -{ - m_NexusInterface->setPluginContainer(pluginContainer); -} - - - - - - -void DownloadManager::refreshList() -{ - try { - //avoid triggering other refreshes - startDisableDirWatcher(); - - int downloadsBefore = m_ActiveDownloads.size(); - - // remove finished downloads - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end();) { - if (((*iter)->m_State == STATE_READY) || ((*iter)->m_State == STATE_INSTALLED) || ((*iter)->m_State == STATE_UNINSTALLED)) { - delete *iter; - iter = m_ActiveDownloads.erase(iter); - } else { - ++iter; - } - } - - QStringList nameFilters(m_SupportedExtensions); - foreach (const QString &extension, m_SupportedExtensions) { - nameFilters.append("*." + extension); - } - - nameFilters.append(QString("*").append(UNFINISHED)); - QDir dir(QDir::fromNativeSeparators(m_OutputDirectory)); - - // find orphaned meta files and delete them (sounds cruel but it's better for everyone) - QStringList orphans; - QStringList metaFiles = dir.entryList(QStringList() << "*.meta"); - foreach (const QString &metaFile, metaFiles) { - QString baseFile = metaFile.left(metaFile.length() - 5); - if (!QFile::exists(dir.absoluteFilePath(baseFile))) { - orphans.append(dir.absoluteFilePath(metaFile)); - } - } - if (orphans.size() > 0) { - qDebug("%d orphaned meta files will be deleted", orphans.size()); - shellDelete(orphans, true); - } - - // add existing downloads to list - foreach (QString file, dir.entryList(nameFilters, QDir::Files, QDir::Time)) { - bool Exists = false; - for (QVector::const_iterator Iter = m_ActiveDownloads.begin(); Iter != m_ActiveDownloads.end() && !Exists; ++Iter) { - if (QString::compare((*Iter)->m_FileName, file, Qt::CaseInsensitive) == 0) { - Exists = true; - } else if (QString::compare(QFileInfo((*Iter)->m_Output.fileName()).fileName(), file, Qt::CaseInsensitive) == 0) { - Exists = true; - } - } - if (Exists) { - continue; - } - - QString fileName = QDir::fromNativeSeparators(m_OutputDirectory) + "/" + file; - - DownloadInfo *info = DownloadInfo::createFromMeta(fileName, m_ShowHidden, m_OutputDirectory); - if (info != nullptr) { - m_ActiveDownloads.push_front(info); - } - } - - //if (m_ActiveDownloads.size() != downloadsBefore) { - qDebug("Downloads after refresh: %d", m_ActiveDownloads.size()); - //} - emit update(-1); - - //let watcher trigger refreshes again - endDisableDirWatcher(); - - } catch (const std::bad_alloc&) { - reportError(tr("Memory allocation error (in refreshing directory).")); - } -} - - -bool DownloadManager::addDownload(const QStringList &URLs, QString gameName, - int modID, int fileID, const ModRepositoryFileInfo *fileInfo) -{ - QString fileName = QFileInfo(URLs.first()).fileName(); - if (fileName.isEmpty()) { - fileName = "unknown"; - } - - QUrl preferredUrl = QUrl::fromEncoded(URLs.first().toLocal8Bit()); - qDebug("selected download url: %s", qUtf8Printable(preferredUrl.toString())); - QNetworkRequest request(preferredUrl); - request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent()); - return addDownload(m_NexusInterface->getAccessManager()->get(request), URLs, fileName, gameName, modID, fileID, fileInfo); -} - - -bool DownloadManager::addDownload(QNetworkReply *reply, const ModRepositoryFileInfo *fileInfo) -{ - QString fileName = getFileNameFromNetworkReply(reply); - if (fileName.isEmpty()) { - fileName = "unknown"; - } - - return addDownload(reply, QStringList(reply->url().toString()), fileName, fileInfo->gameName, fileInfo->modID, fileInfo->fileID, fileInfo); -} - - -bool DownloadManager::addDownload(QNetworkReply *reply, const QStringList &URLs, const QString &fileName, - QString gameName, int modID, int fileID, const ModRepositoryFileInfo *fileInfo) -{ - // download invoked from an already open network reply (i.e. download link in the browser) - DownloadInfo *newDownload = DownloadInfo::createNew(fileInfo, URLs); - - QString baseName = fileName; - if (!fileInfo->fileName.isEmpty()) { - baseName = fileInfo->fileName; - } else { - QString dispoName = getFileNameFromNetworkReply(reply); - - if (!dispoName.isEmpty()) { - baseName = dispoName; - } - } - - startDisableDirWatcher(); - newDownload->setName(getDownloadFileName(baseName), false); - endDisableDirWatcher(); - - startDownload(reply, newDownload, false); -// emit update(-1); - return true; -} - - -void DownloadManager::removePending(QString gameName, int modID, int fileID) -{ - emit aboutToUpdate(); - for (auto iter : m_PendingDownloads) { - if (gameName.compare(std::get<0>(iter), Qt::CaseInsensitive) == 0 && (std::get<1>(iter) == modID) && (std::get<2>(iter) == fileID)) { - m_PendingDownloads.removeAt(m_PendingDownloads.indexOf(iter)); - break; - } - } - emit update(-1); -} - - -void DownloadManager::startDownload(QNetworkReply *reply, DownloadInfo *newDownload, bool resume) -{ - reply->setReadBufferSize(1024 * 1024); // don't read more than 1MB at once to avoid memory troubles - newDownload->m_Reply = reply; - setState(newDownload, STATE_DOWNLOADING); - if (newDownload->m_Urls.count() == 0) { - newDownload->m_Urls = QStringList(reply->url().toString()); - } - - QIODevice::OpenMode mode = QIODevice::WriteOnly; - if (resume) { - mode |= QIODevice::Append; - } - - newDownload->m_StartTime.start(); - createMetaFile(newDownload); - - if (!newDownload->m_Output.open(mode)) { - reportError(tr("failed to download %1: could not open output file: %2") - .arg(reply->url().toString()).arg(newDownload->m_Output.fileName())); - return; - } - - connect(newDownload->m_Reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(downloadProgress(qint64, qint64))); - connect(newDownload->m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); - connect(newDownload->m_Reply, SIGNAL(readyRead()), this, SLOT(downloadReadyRead())); - connect(newDownload->m_Reply, SIGNAL(metaDataChanged()), this, SLOT(metaDataChanged())); - - if (!resume) { - newDownload->m_PreResumeSize = newDownload->m_Output.size(); - removePending(newDownload->m_FileInfo->gameName, newDownload->m_FileInfo->modID, newDownload->m_FileInfo->fileID); - - emit aboutToUpdate(); - m_ActiveDownloads.append(newDownload); - - emit update(-1); - emit downloadAdded(); - - if (QFile::exists(m_OutputDirectory + "/" + newDownload->m_FileName)) { - setState(newDownload, STATE_PAUSING); - QCoreApplication::processEvents(); - if (QMessageBox::question(nullptr, tr("Download again?"), tr("A file with the same name \"%1\" has already been downloaded. " - "Do you want to download it again? The new file will receive a different name.").arg(newDownload->m_FileName), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { - if (reply->isFinished()) - setState(newDownload, STATE_CANCELED); - else - setState(newDownload, STATE_CANCELING); - } else { - startDisableDirWatcher(); - newDownload->setName(getDownloadFileName(newDownload->m_FileName, true), true); - endDisableDirWatcher(); - if (newDownload->m_State == STATE_PAUSED) - resumeDownload(indexByName(newDownload->m_FileName)); - else - setState(newDownload, STATE_DOWNLOADING); - } - } else - connect(newDownload->m_Reply, SIGNAL(finished()), this, SLOT(downloadFinished())); - - - QCoreApplication::processEvents(); - - if (newDownload->m_State != STATE_DOWNLOADING && - newDownload->m_State != STATE_READY && - newDownload->m_State != STATE_FETCHINGMODINFO && - reply->isFinished()) { - downloadFinished(indexByName(newDownload->m_FileName)); - return; - } - } else - connect(newDownload->m_Reply, SIGNAL(finished()), this, SLOT(downloadFinished())); -} - - -void DownloadManager::addNXMDownload(const QString &url) -{ - NXMUrl nxmInfo(url); - - QStringList validGames; - validGames.append(m_ManagedGame->gameShortName()); - validGames.append(m_ManagedGame->validShortNames()); - qDebug("add nxm download: %s", qUtf8Printable(url)); - if (!validGames.contains(nxmInfo.game(), Qt::CaseInsensitive)) { - qDebug("download requested for wrong game (game: %s, url: %s)", qUtf8Printable(m_ManagedGame->gameShortName()), qUtf8Printable(nxmInfo.game())); - QMessageBox::information(nullptr, tr("Wrong Game"), tr("The download link is for a mod for \"%1\" but this instance of MO " - "has been set up for \"%2\".").arg(nxmInfo.game()).arg(m_ManagedGame->gameShortName()), QMessageBox::Ok); - return; - } - - for (auto tuple : m_PendingDownloads) { - if (std::get<0>(tuple).compare(nxmInfo.game(), Qt::CaseInsensitive) == 0, std::get<1>(tuple) == nxmInfo.modId() && std::get<2>(tuple) == nxmInfo.fileId()) { - qDebug("download requested is already started (mod id: %s, file id: %s)", qUtf8Printable(QString(nxmInfo.modId())), qUtf8Printable(QString(nxmInfo.fileId()))); - QMessageBox::information(nullptr, tr("Already Started"), tr("A download for this mod file has already been queued."), QMessageBox::Ok); - return; - } - } - - for (DownloadInfo *download : m_ActiveDownloads) { - if (download->m_FileInfo->modID == nxmInfo.modId() && download->m_FileInfo->fileID == nxmInfo.fileId()) { - if (download->m_State == STATE_DOWNLOADING || download->m_State == STATE_PAUSED || download->m_State == STATE_STARTED) { - qDebug("download requested is already started (mod: %s, file: %s)", qUtf8Printable(QString(download->m_FileInfo->modID)), - qUtf8Printable(download->m_FileInfo->fileName)); - - QMessageBox::information(nullptr, tr("Already Started"), tr("There is already a download started for this file (mod: %1, file: %2).") - .arg(download->m_FileInfo->modName).arg(download->m_FileInfo->fileName), QMessageBox::Ok); - return; - } - } - } - - emit aboutToUpdate(); - - m_PendingDownloads.append(std::make_tuple(nxmInfo.game(), nxmInfo.modId(), nxmInfo.fileId())); - - emit update(-1); - emit downloadAdded(); - m_RequestIDs.insert(m_NexusInterface->requestFileInfo(nxmInfo.game(), nxmInfo.modId(), nxmInfo.fileId(), this, nxmInfo.fileId(), "")); -} - - -void DownloadManager::removeFile(int index, bool deleteFile) -{ - //Avoid triggering refreshes from DirWatcher - startDisableDirWatcher(); - - if (index >= m_ActiveDownloads.size()) { - throw MyException(tr("remove: invalid download index %1").arg(index)); - } - - DownloadInfo *download = m_ActiveDownloads.at(index); - QString filePath = m_OutputDirectory + "/" + download->m_FileName; - if ((download->m_State == STATE_STARTED) || - (download->m_State == STATE_DOWNLOADING)) { - // shouldn't have been possible - qCritical("tried to remove active download"); - endDisableDirWatcher(); - return; - } - - if ((download->m_State == STATE_PAUSED) || (download->m_State == STATE_ERROR)) { - filePath = download->m_Output.fileName(); - } - - if (deleteFile) { - if (!shellDelete(QStringList(filePath), true)) { - reportError(tr("failed to delete %1").arg(filePath)); - endDisableDirWatcher(); - return; - } - - QFile metaFile(filePath.append(".meta")); - if (metaFile.exists() && !shellDelete(QStringList(filePath), true)) { - reportError(tr("failed to delete meta file for %1").arg(filePath)); - } - } else { - QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat); - if(!download->m_Hidden) - metaSettings.setValue("removed", true); - } - - endDisableDirWatcher(); -} - -class LessThanWrapper -{ -public: - LessThanWrapper(DownloadManager *manager) : m_Manager(manager) {} - bool operator()(int LHS, int RHS) { - return m_Manager->getFileName(LHS).compare(m_Manager->getFileName(RHS), Qt::CaseInsensitive) < 0; - - } - -private: - DownloadManager *m_Manager; -}; - - -bool DownloadManager::ByName(int LHS, int RHS) -{ - return m_ActiveDownloads[LHS]->m_FileName < m_ActiveDownloads[RHS]->m_FileName; -} - - -void DownloadManager::refreshAlphabeticalTranslation() -{ - m_AlphabeticalTranslation.clear(); - int pos = 0; - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++pos) { - m_AlphabeticalTranslation.push_back(pos); - } - - qSort(m_AlphabeticalTranslation.begin(), m_AlphabeticalTranslation.end(), LessThanWrapper(this)); -} - - -void DownloadManager::restoreDownload(int index) -{ - - if (index < 0) { - DownloadState minState = STATE_READY ; - index = 0; - - for (QVector::const_iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter ) { - - if ((*iter)->m_State >= minState) { - restoreDownload(index); - } - index++; - } - } - else { - if (index >= m_ActiveDownloads.size()) { - throw MyException(tr("restore: invalid download index: %1").arg(index)); - } - - DownloadInfo *download = m_ActiveDownloads.at(index); - if (download->m_Hidden) { - download->m_Hidden = false; - - QString filePath = m_OutputDirectory + "/" + download->m_FileName; - - //avoid dirWatcher triggering refreshes - startDisableDirWatcher(); - QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat); - metaSettings.setValue("removed", false); - - endDisableDirWatcher(); - } - } -} - - -void DownloadManager::removeDownload(int index, bool deleteFile) -{ - try { - //avoid dirWatcher triggering refreshes - startDisableDirWatcher(); - - emit aboutToUpdate(); - - if (index < 0) { - bool removeAll = (index == -1); - DownloadState removeState = (index == -2 ? STATE_INSTALLED : STATE_UNINSTALLED); - - index = 0; - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end();) { - DownloadState downloadState = (*iter)->m_State; - if ((removeAll && (downloadState >= STATE_READY)) || - (removeState == downloadState)) { - removeFile(index, deleteFile); - delete *iter; - iter = m_ActiveDownloads.erase(iter); - } else { - ++iter; - ++index; - } - } - } else { - if (index >= m_ActiveDownloads.size()) { - reportError(tr("remove: invalid download index %1").arg(index)); - //emit update(-1); - endDisableDirWatcher(); - return; - } - - removeFile(index, deleteFile); - delete m_ActiveDownloads.at(index); - m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index); - } - emit update(-1); - endDisableDirWatcher(); - } catch (const std::exception &e) { - qCritical("failed to remove download: %s", e.what()); - } - refreshList(); -} - - -void DownloadManager::cancelDownload(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("cancel: invalid download index %1").arg(index)); - return; - } - - if (m_ActiveDownloads.at(index)->m_State == STATE_DOWNLOADING) { - setState(m_ActiveDownloads.at(index), STATE_CANCELING); - } -} - - -void DownloadManager::pauseDownload(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("pause: invalid download index %1").arg(index)); - return; - } - - DownloadInfo *info = m_ActiveDownloads.at(index); - - if (info->m_State == STATE_DOWNLOADING) { - if ((info->m_Reply != nullptr) && (info->m_Reply->isRunning())) { - setState(info, STATE_PAUSING); - } else { - setState(info, STATE_PAUSED); - } - } else if ((info->m_State == STATE_FETCHINGMODINFO) || (info->m_State == STATE_FETCHINGFILEINFO)) { - setState(info, STATE_READY); - } -} - -void DownloadManager::resumeDownload(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("resume: invalid download index %1").arg(index)); - return; - } - DownloadInfo *info = m_ActiveDownloads[index]; - info->m_Tries = AUTOMATIC_RETRIES; - resumeDownloadInt(index); -} - -void DownloadManager::resumeDownloadInt(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("resume (int): invalid download index %1").arg(index)); - return; - } - DownloadInfo *info = m_ActiveDownloads[index]; - - // Check for finished download; - if (info->m_TotalSize <= info->m_Output.size() && info->m_Reply != nullptr - && info->m_Reply->isOpen() && info->m_Reply->isFinished() && info->m_State != STATE_ERROR) { - setState(info, STATE_DOWNLOADING); - downloadFinished(index); - return; - } - - if (info->isPausedState() || info->m_State == STATE_PAUSING) { - if (info->m_State == STATE_PAUSING) { - if (info->m_Output.isOpen()) { - writeData(info); - if (info->m_State == STATE_PAUSING) { - setState(info, STATE_PAUSED); - } - } - } - if ((info->m_Urls.size() == 0) - || ((info->m_Urls.size() == 1) && (info->m_Urls[0].size() == 0))) { - emit showMessage(tr("No known download urls. Sorry, this download can't be resumed.")); - return; - } - if (info->m_State == STATE_ERROR) { - info->m_CurrentUrl = (info->m_CurrentUrl + 1) % info->m_Urls.count(); - } - qDebug("request resume from url %s", qUtf8Printable(info->currentURL())); - QNetworkRequest request(QUrl::fromEncoded(info->currentURL().toLocal8Bit())); - request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent()); - if (info->m_State != STATE_ERROR) { - info->m_ResumePos = info->m_Output.size(); - QByteArray rangeHeader = "bytes=" + QByteArray::number(info->m_ResumePos) + "-"; - request.setRawHeader("Range", rangeHeader); - } - std::get<0>(info->m_SpeedDiff) = 0; - std::get<1>(info->m_SpeedDiff) = 0; - std::get<2>(info->m_SpeedDiff) = 0; - std::get<3>(info->m_SpeedDiff) = 0; - std::get<4>(info->m_SpeedDiff) = 0; - qDebug("resume at %lld bytes", info->m_ResumePos); - startDownload(m_NexusInterface->getAccessManager()->get(request), info, true); - } - emit update(index); -} - - -DownloadManager::DownloadInfo *DownloadManager::downloadInfoByID(unsigned int id) -{ - auto iter = std::find_if(m_ActiveDownloads.begin(), m_ActiveDownloads.end(), - [id](DownloadInfo *info) { return info->m_DownloadID == id; }); - if (iter != m_ActiveDownloads.end()) { - return *iter; - } else { - return nullptr; - } -} - - -void DownloadManager::queryInfo(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("query: invalid download index %1").arg(index)); - return; - } - DownloadInfo *info = m_ActiveDownloads[index]; - - if (info->m_FileInfo->repository != "Nexus") { - qWarning("re-querying file info is currently only possible with Nexus"); - return; - } - - if (info->m_State < DownloadManager::STATE_READY) { - // UI shouldn't allow this - return; - } - - if (info->m_FileInfo->modID <= 0) { - QString fileName = getFileName(index); - QString ignore; - NexusInterface::interpretNexusFileName(fileName, ignore, info->m_FileInfo->modID, true); - if (info->m_FileInfo->modID < 0) { - bool ok = false; - int modId = QInputDialog::getInt( - nullptr, tr("Please enter the nexus mod id"), tr("Mod ID:"), 1, 1, - std::numeric_limits::max(), 1, &ok); - // careful now: while the dialog was displayed, events were processed. - // the download list might have changed and our info-ptr invalidated. - if (ok) - m_ActiveDownloads[index]->m_FileInfo->modID = modId; - return; - } - } - - if (info->m_FileInfo->gameName.size() == 0) { - SelectionDialog selection(tr("Please select the source game code for %1").arg(getFileName(index))); - - std::vector> choices = m_NexusInterface->getGameChoices(m_ManagedGame); - for (auto choice : choices) { - selection.addChoice(choice.first, choice.second, choice.first); - } - if (selection.exec() == QDialog::Accepted) { - info->m_FileInfo->gameName = selection.getChoiceData().toString(); - } else { - info->m_FileInfo->gameName = m_ManagedGame->gameShortName(); - } - } - info->m_ReQueried = true; - setState(info, STATE_FETCHINGMODINFO); -} - -void DownloadManager::visitOnNexus(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("VisitNexus: invalid download index %1").arg(index)); - return; - } - DownloadInfo *info = m_ActiveDownloads[index]; - - if (info->m_FileInfo->repository != "Nexus") { - qWarning("Visiting mod page is currently only possible with Nexus"); - return; - } - - if (info->m_State < DownloadManager::STATE_READY) { - // UI shouldn't allow this - return; - } - int modID = info->m_FileInfo->modID; - - QString gameName = info->m_FileInfo->gameName; - if (modID > 0) { - QDesktopServices::openUrl(QUrl(m_NexusInterface->getModURL(modID, gameName))); - } - else { - emit showMessage(tr("Nexus ID for this Mod is unknown")); - } -} - -void DownloadManager::openFile(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("OpenFile: invalid download index %1").arg(index)); - return; - } - QDir path = QDir(m_OutputDirectory); - if (path.exists(getFileName(index))) { - - ::ShellExecuteW(nullptr, L"open", ToWString(QDir::toNativeSeparators(getFilePath(index))).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - return; - } - - ::ShellExecuteW(nullptr, L"explore", ToWString(QDir::toNativeSeparators(m_OutputDirectory)).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - return; -} - -void DownloadManager::openInDownloadsFolder(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("OpenFileInDownloadsFolder: invalid download index %1").arg(index)); - return; - } - QString params = "/select,\""; - QDir path = QDir(m_OutputDirectory); - if (path.exists(getFileName(index))) { - params = params + QDir::toNativeSeparators(getFilePath(index)) + "\""; - - ::ShellExecuteW(nullptr, nullptr, L"explorer", ToWString(params).c_str(), nullptr, SW_SHOWNORMAL); - return; - } - else if (path.exists(getFileName(index) + ".unfinished")) { - params = params + QDir::toNativeSeparators(getFilePath(index)+".unfinished") + "\""; - - ::ShellExecuteW(nullptr, nullptr, L"explorer", ToWString(params).c_str(), nullptr, SW_SHOWNORMAL); - return; - } - - ::ShellExecuteW(nullptr, L"explore", ToWString(QDir::toNativeSeparators(m_OutputDirectory)).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - return; -} - - -int DownloadManager::numTotalDownloads() const -{ - return m_ActiveDownloads.size(); -} - -int DownloadManager::numPendingDownloads() const -{ - return m_PendingDownloads.size(); -} - -std::tuple DownloadManager::getPendingDownload(int index) -{ - if ((index < 0) || (index >= m_PendingDownloads.size())) { - throw MyException(tr("get pending: invalid download index %1").arg(index)); - } - - return m_PendingDownloads.at(index); -} - -QString DownloadManager::getFilePath(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("get path: invalid download index %1").arg(index)); - } - - return m_OutputDirectory + "/" + m_ActiveDownloads.at(index)->m_FileName; -} - -QString DownloadManager::getFileTypeString(int fileType) -{ - switch (fileType) { - case 1: return tr("Main"); - case 2: return tr("Update"); - case 3: return tr("Optional"); - case 4: return tr("Old"); - case 5: return tr("Misc"); - default: return tr("Unknown"); - } -} - -QString DownloadManager::getDisplayName(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("display name: invalid download index %1").arg(index)); - } - - DownloadInfo *info = m_ActiveDownloads.at(index); - - QTextDocument doc; - if (!info->m_FileInfo->name.isEmpty()) { - doc.setHtml(info->m_FileInfo->name); - return QString("%1 (%2, v%3)").arg(doc.toPlainText()) - .arg(getFileTypeString(info->m_FileInfo->fileCategory)) - .arg(info->m_FileInfo->version.displayString()); - } else { - doc.setHtml(info->m_FileName); - return doc.toPlainText(); - } -} - -QString DownloadManager::getFileName(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("file name: invalid download index %1").arg(index)); - } - - return m_ActiveDownloads.at(index)->m_FileName; -} - -QDateTime DownloadManager::getFileTime(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("file time: invalid download index %1").arg(index)); - } - - DownloadInfo *info = m_ActiveDownloads.at(index); - if (!info->m_Created.isValid()) { - info->m_Created = QFileInfo(info->m_Output).created(); - } - - return info->m_Created; -} - -qint64 DownloadManager::getFileSize(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("file size: invalid download index %1").arg(index)); - } - - return m_ActiveDownloads.at(index)->m_TotalSize; -} - - -std::pair DownloadManager::getProgress(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("progress: invalid download index %1").arg(index)); - } - - return m_ActiveDownloads.at(index)->m_Progress; -} - - -DownloadManager::DownloadState DownloadManager::getState(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("state: invalid download index %1").arg(index)); - } - - return m_ActiveDownloads.at(index)->m_State; -} - - -bool DownloadManager::isInfoIncomplete(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("infocomplete: invalid download index %1").arg(index)); - } - - DownloadInfo *info = m_ActiveDownloads.at(index); - if (info->m_FileInfo->repository != "Nexus") { - // other repositories currently don't support re-querying info anyway - return false; - } - return (info->m_FileInfo->fileID == 0) || (info->m_FileInfo->modID == 0); -} - - -int DownloadManager::getModID(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("mod id: invalid download index %1").arg(index)); - } - return m_ActiveDownloads.at(index)->m_FileInfo->modID; -} - -QString DownloadManager::getGameName(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("mod id: invalid download index %1").arg(index)); - } - return m_ActiveDownloads.at(index)->m_FileInfo->gameName; -} - -bool DownloadManager::isHidden(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("ishidden: invalid download index %1").arg(index)); - } - return m_ActiveDownloads.at(index)->m_Hidden; -} - - -const ModRepositoryFileInfo *DownloadManager::getFileInfo(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("file info: invalid download index %1").arg(index)); - } - - return m_ActiveDownloads.at(index)->m_FileInfo; -} - - -void DownloadManager::markInstalled(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("mark installed: invalid download index %1").arg(index)); - } - - //Avoid triggering refreshes from DirWatcher - startDisableDirWatcher(); - - DownloadInfo *info = m_ActiveDownloads.at(index); - QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); - metaFile.setValue("installed", true); - metaFile.setValue("uninstalled", false); - - endDisableDirWatcher(); - - setState(m_ActiveDownloads.at(index), STATE_INSTALLED); -} - -void DownloadManager::markInstalled(QString fileName) -{ - int index = indexByName(fileName); - if (index >= 0) { - markInstalled(index); - } else { - DownloadInfo *info = getDownloadInfo(fileName); - if (info != nullptr) { - //Avoid triggering refreshes from DirWatcher - startDisableDirWatcher(); - - QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); - metaFile.setValue("installed", true); - metaFile.setValue("uninstalled", false); - delete info; - - endDisableDirWatcher(); - } - } -} - -DownloadManager::DownloadInfo* DownloadManager::getDownloadInfo(QString fileName) -{ - return DownloadInfo::createFromMeta(fileName, true, m_OutputDirectory); -} - -void DownloadManager::markUninstalled(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("mark uninstalled: invalid download index %1").arg(index)); - } - - //Avoid triggering refreshes from DirWatcher - startDisableDirWatcher(); - - DownloadInfo *info = m_ActiveDownloads.at(index); - QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); - metaFile.setValue("uninstalled", true); - - endDisableDirWatcher(); - - setState(m_ActiveDownloads.at(index), STATE_UNINSTALLED); -} - - -void DownloadManager::markUninstalled(QString fileName) -{ - int index = indexByName(fileName); - if (index >= 0) { - markUninstalled(index); - } else { - QString filePath = QDir::fromNativeSeparators(m_OutputDirectory) + "/" + fileName; - DownloadInfo *info = getDownloadInfo(filePath); - if (info != nullptr) { - - //Avoid triggering refreshes from DirWatcher - startDisableDirWatcher(); - - QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); - metaFile.setValue("uninstalled", true); - delete info; - - endDisableDirWatcher(); - } - } -} - - -QString DownloadManager::getDownloadFileName(const QString &baseName, bool rename) const -{ - QString fullPath = m_OutputDirectory + "/" + baseName; - if (QFile::exists(fullPath) && rename) { - int i = 1; - while (QFile::exists(QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName))) { - ++i; - } - - fullPath = QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName); - } - return fullPath; -} - - -QString DownloadManager::getFileNameFromNetworkReply(QNetworkReply *reply) -{ - if (reply->hasRawHeader("Content-Disposition")) { - std::regex exp("filename=\"(.*)\""); - - std::cmatch result; - if (std::regex_search(reply->rawHeader("Content-Disposition").constData(), result, exp)) { - return QString::fromUtf8(result.str(1).c_str()); - } - } - - return QString(); -} - - -void DownloadManager::setState(DownloadManager::DownloadInfo *info, DownloadManager::DownloadState state) -{ - int row = 0; - for (int i = 0; i < m_ActiveDownloads.size(); ++i) { - if (m_ActiveDownloads[i] == info) { - row = i; - break; - } - } - info->m_State = state; - switch (state) { - case STATE_PAUSED: - case STATE_ERROR: { - info->m_Reply->abort(); - info->m_Output.close(); - } break; - case STATE_CANCELED: { - info->m_Reply->abort(); - } break; - case STATE_FETCHINGMODINFO: { - m_RequestIDs.insert(m_NexusInterface->requestDescription(info->m_FileInfo->gameName, info->m_FileInfo->modID, this, info->m_DownloadID, QString())); - } break; - case STATE_FETCHINGFILEINFO: { - m_RequestIDs.insert(m_NexusInterface->requestFiles(info->m_FileInfo->gameName, info->m_FileInfo->modID, this, info->m_DownloadID, QString())); - } break; - case STATE_READY: { - createMetaFile(info); - emit downloadComplete(row); - } break; - default: /* NOP */ break; - } - emit stateChanged(row, state); -} - - -DownloadManager::DownloadInfo *DownloadManager::findDownload(QObject *reply, int *index) const -{ - // reverse search as newer, thus more relevant, downloads are at the end - for (int i = m_ActiveDownloads.size() - 1; i >= 0; --i) { - if (m_ActiveDownloads[i]->m_Reply == reply) { - if (index != nullptr) { - *index = i; - } - return m_ActiveDownloads[i]; - } - } - return nullptr; -} - - -void DownloadManager::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) -{ - if (bytesTotal == 0) { - return; - } - int index = 0; - try { - DownloadInfo *info = findDownload(this->sender(), &index); - if (info != nullptr) { - info->m_HasData = true; - if (info->m_State == STATE_CANCELING) { - setState(info, STATE_CANCELED); - } else if (info->m_State == STATE_PAUSING) { - setState(info, STATE_PAUSED); - } - else { - if (bytesTotal > info->m_TotalSize) { - info->m_TotalSize = bytesTotal; - } - int oldProgress = info->m_Progress.first; - info->m_Progress.first = ((info->m_ResumePos + bytesReceived) * 100) / (info->m_ResumePos + bytesTotal); - - int elapsed = info->m_StartTime.elapsed(); - std::get<0>(info->m_SpeedDiff) = bytesReceived - std::get<2>(info->m_SpeedDiff); - std::get<1>(info->m_SpeedDiff) = elapsed - std::get<3>(info->m_SpeedDiff); - std::get<2>(info->m_SpeedDiff) = bytesReceived; - std::get<3>(info->m_SpeedDiff) = elapsed; - - double calc = ((double)std::get<0>(info->m_SpeedDiff)) / (((double)(std::get<1>(info->m_SpeedDiff)) / 5000.0)); - std::get<4>(info->m_SpeedDiff) = ((calc*0.5) + (std::get<4>(info->m_SpeedDiff)*1.5)) / 2; - - // calculate the download speed - double speed = (std::get<4>(info->m_SpeedDiff) * 1000.0) / (5 * 1000); - - QString unit; - if (speed < 1000) { - unit = "B/s"; - } - else if (speed < 1000*1024) { - speed /= 1024; - unit = "KB/s"; - } - else { - speed /= 1024 * 1024; - unit = "MB/s"; - } - - info->m_Progress.second = QString::fromLatin1("%1% - %2 %3").arg(info->m_Progress.first).arg(QString::number(speed, 'f', 1)).arg(unit); - - TaskProgressManager::instance().updateProgress(info->m_TaskProgressId, bytesReceived, bytesTotal); - emit update(index); - } - } - } catch (const std::bad_alloc&) { - reportError(tr("Memory allocation error (in processing progress event).")); - } -} - - -void DownloadManager::downloadReadyRead() -{ - try { - writeData(findDownload(this->sender())); - } catch (const std::bad_alloc&) { - reportError(tr("Memory allocation error (in processing downloaded data).")); - } -} - - -void DownloadManager::createMetaFile(DownloadInfo *info) -{ - //Avoid triggering refreshes from DirWatcher - startDisableDirWatcher(); - - QSettings metaFile(QString("%1.meta").arg(info->m_Output.fileName()), QSettings::IniFormat); - metaFile.setValue("gameName", info->m_FileInfo->gameName); - metaFile.setValue("modID", info->m_FileInfo->modID); - metaFile.setValue("fileID", info->m_FileInfo->fileID); - metaFile.setValue("url", info->m_Urls.join(";")); - metaFile.setValue("name", info->m_FileInfo->name); - metaFile.setValue("description", info->m_FileInfo->description); - metaFile.setValue("modName", info->m_FileInfo->modName); - metaFile.setValue("version", info->m_FileInfo->version.canonicalString()); - metaFile.setValue("newestVersion", info->m_FileInfo->newestVersion.canonicalString()); - metaFile.setValue("fileTime", info->m_FileInfo->fileTime); - metaFile.setValue("fileCategory", info->m_FileInfo->fileCategory); - metaFile.setValue("category", info->m_FileInfo->categoryID); - metaFile.setValue("repository", info->m_FileInfo->repository); - metaFile.setValue("userData", info->m_FileInfo->userData); - metaFile.setValue("installed", info->m_State == DownloadManager::STATE_INSTALLED); - metaFile.setValue("uninstalled", info->m_State == DownloadManager::STATE_UNINSTALLED); - metaFile.setValue("paused", (info->m_State == DownloadManager::STATE_PAUSED) || - (info->m_State == DownloadManager::STATE_ERROR)); - metaFile.setValue("removed", info->m_Hidden); - - endDisableDirWatcher(); - // slightly hackish... - for (int i = 0; i < m_ActiveDownloads.size(); ++i) { - if (m_ActiveDownloads[i] == info) { - emit update(i); - } - } -} - - -void DownloadManager::nxmDescriptionAvailable(QString, int, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - QVariantMap result = resultData.toMap(); - - DownloadInfo *info = downloadInfoByID(userData.toInt()); - if (info == nullptr) return; - info->m_FileInfo->categoryID = result["category_id"].toInt(); - QTextDocument doc; - doc.setHtml(result["name"].toString().trimmed()); - info->m_FileInfo->modName = doc.toPlainText(); - info->m_FileInfo->newestVersion.parse(result["version"].toString()); - if (info->m_FileInfo->fileID != 0) { - setState(info, STATE_READY); - } else { - setState(info, STATE_FETCHINGFILEINFO); - } -} - - -QDateTime DownloadManager::matchDate(const QString &timeString) -{ - if (m_DateExpression.exactMatch(timeString)) { - return QDateTime::fromMSecsSinceEpoch(m_DateExpression.cap(1).toLongLong()); - } else { - qWarning("date not matched: %s", qUtf8Printable(timeString)); - return QDateTime::currentDateTime(); - } -} - - -void DownloadManager::nxmFilesAvailable(QString, int, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - DownloadInfo *info = downloadInfoByID(userData.toInt()); - if (info == nullptr) return; - - QVariantList result = resultData.toList(); - - // MO sometimes prepends _ to the filename in case of duplicate downloads. - // this may muck up the file name comparison - QString alternativeLocalName = info->m_FileName; - - QRegExp expression("^\\d_(.*)$"); - if (expression.indexIn(alternativeLocalName) == 0) { - alternativeLocalName = expression.cap(1); - } - - bool found = false; - - for (QVariant file : result) { - QVariantMap fileInfo = file.toMap(); - QString fileName = fileInfo["uri"].toString(); - QString fileNameVariant = fileName.mid(0).replace(' ', '_'); - if ((fileName == info->m_FileName) || (fileName == alternativeLocalName) || - (fileNameVariant == info->m_FileName) || (fileNameVariant == alternativeLocalName)) { - info->m_FileInfo->name = fileInfo["name"].toString(); - info->m_FileInfo->version.parse(fileInfo["version"].toString()); - if (!info->m_FileInfo->version.isValid()) { - info->m_FileInfo->version = info->m_FileInfo->newestVersion; - } - info->m_FileInfo->fileCategory = fileInfo["category_id"].toInt(); - info->m_FileInfo->fileTime = matchDate(fileInfo["date"].toString()); - info->m_FileInfo->fileID = fileInfo["id"].toInt(); - info->m_FileInfo->fileName = fileInfo["uri"].toString(); - info->m_FileInfo->description = BBCode::convertToHTML(fileInfo["description"].toString()); - found = true; - break; - } - } - - if (info->m_ReQueried) { - if (found) { - emit showMessage(tr("Information updated")); - } else if (result.count() == 0) { - emit showMessage(tr("No matching file found on Nexus! Maybe this file is no longer available or it was renamed?")); - } else { - SelectionDialog selection(tr("No file on Nexus matches the selected file by name. Please manually choose the correct one.")); - for (QVariant file : result) { - QVariantMap fileInfo = file.toMap(); - selection.addChoice(fileInfo["uri"].toString(), "", file); - } - if (selection.exec() == QDialog::Accepted) { - QVariantMap fileInfo = selection.getChoiceData().toMap(); - info->m_FileInfo->name = fileInfo["name"].toString(); - info->m_FileInfo->version.parse(fileInfo["version"].toString()); - info->m_FileInfo->fileCategory = fileInfo["category_id"].toInt(); - info->m_FileInfo->fileID = fileInfo["id"].toInt(); - } else { - emit showMessage(tr("No matching file found on Nexus! Maybe this file is no longer available or it was renamed?")); - } - } - } else { - if (info->m_FileInfo->fileID == 0) { - qWarning("could not determine file id for %s (state %d)", - qUtf8Printable(info->m_FileName), info->m_State); - } - } - - setState(info, STATE_READY); -} - - -void DownloadManager::nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - ModRepositoryFileInfo *info = new ModRepositoryFileInfo(); - - QVariantMap result = resultData.toMap(); - info->name = result["name"].toString(); - info->version.parse(result["version"].toString()); - if (!info->version.isValid()) { - info->version = info->newestVersion; - } - info->fileName = result["file_name"].toString(); - info->fileCategory = result["category_id"].toInt(); - info->fileTime = matchDate(result["uploaded_timestamp"].toString()); - info->description = BBCode::convertToHTML(result["changelog_html"].toString()); - - info->repository = "Nexus"; - info->gameName = gameName; - info->modID = modID; - info->fileID = fileID; - - QObject *test = info; - m_RequestIDs.insert(m_NexusInterface->requestDownloadURL(gameName, modID, fileID, this, qVariantFromValue(test), QString())); -} - -static int evaluateFileInfoMap(const QVariantMap &map, const std::map &preferredServers) -{ - int result = 0; - - auto preference = preferredServers.find(map["name"].toString()); - - if (preference != preferredServers.end()) { - result += 100 + preference->second * 20; - } - - return result; -} - -// sort function to sort by best download server -bool DownloadManager::ServerByPreference(const std::map &preferredServers, const QVariant &LHS, const QVariant &RHS) -{ - return evaluateFileInfoMap(LHS.toMap(), preferredServers) > evaluateFileInfoMap(RHS.toMap(), preferredServers); -} - -int DownloadManager::startDownloadURLs(const QStringList &urls) -{ - ModRepositoryFileInfo info; - addDownload(urls, "", -1, -1, &info); - return m_ActiveDownloads.size() - 1; -} - -int DownloadManager::startDownloadNexusFile(int modID, int fileID) -{ - int newID = m_ActiveDownloads.size(); - addNXMDownload(QString("nxm://%1/mods/%2/files/%3").arg(m_ManagedGame->gameShortName()).arg(modID).arg(fileID)); - return newID; -} - -QString DownloadManager::downloadPath(int id) -{ - return getFilePath(id); -} - -int DownloadManager::indexByName(const QString &fileName) const -{ - for (int i = 0; i < m_ActiveDownloads.size(); ++i) { - if (m_ActiveDownloads[i]->m_FileName == fileName) { - return i; - } - } - return -1; -} - -void DownloadManager::nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - ModRepositoryFileInfo *info = qobject_cast(qvariant_cast(userData)); - QVariantList resultList = resultData.toList(); - if (resultList.length() == 0) { - removePending(gameName, modID, fileID); - emit showMessage(tr("No download server available. Please try again later.")); - return; - } - - std::sort(resultList.begin(), resultList.end(), boost::bind(&DownloadManager::ServerByPreference, m_PreferredServers, _1, _2)); - - info->userData["downloadMap"] = resultList; - - QStringList URLs; - - foreach (const QVariant &server, resultList) { - URLs.append(server.toMap()["URI"].toString()); - } - addDownload(URLs, gameName, modID, fileID, info); -} - - -void DownloadManager::nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, const QString &errorString) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - int index = 0; - - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++index) { - DownloadInfo *info = *iter; - if (info->m_FileInfo->modID == modID) { - if (info->m_State < STATE_FETCHINGMODINFO) { - m_ActiveDownloads.erase(iter); - delete info; - } else { - setState(info, STATE_READY); - } - emit update(index); - break; - } - } - - removePending(gameName, modID, fileID); - emit showMessage(tr("Failed to request file info from nexus: %1").arg(errorString)); -} - - -void DownloadManager::downloadFinished(int index) -{ - DownloadInfo *info; - if (index) - info = m_ActiveDownloads[index]; - else - info = findDownload(this->sender(), &index); - - if (info != nullptr) { - QNetworkReply *reply = info->m_Reply; - QByteArray data; - if (reply->isOpen() && info->m_HasData) { - data = reply->readAll(); - info->m_Output.write(data); - } - info->m_Output.close(); - TaskProgressManager::instance().forgetMe(info->m_TaskProgressId); - - bool error = false; - if ((info->m_State != STATE_CANCELING) && - (info->m_State != STATE_PAUSING)) { - bool textData = reply->header(QNetworkRequest::ContentTypeHeader).toString().startsWith("text", Qt::CaseInsensitive); - if (textData) - emit showMessage(tr("Warning: Content type is: %1").arg(reply->header(QNetworkRequest::ContentTypeHeader).toString())); - if ((info->m_Output.size() == 0) || - ((reply->error() != QNetworkReply::NoError) - && (reply->error() != QNetworkReply::OperationCanceledError))) { - if (reply->error() == QNetworkReply::UnknownContentError) - emit showMessage(tr("Download header content length: %1 downloaded file size: %2").arg(reply->header(QNetworkRequest::ContentLengthHeader).toLongLong()).arg(info->m_Output.size())); - if (info->m_Tries == 0) { - emit showMessage(tr("Download failed: %1 (%2)").arg(reply->errorString()).arg(reply->error())); - } - error = true; - setState(info, STATE_ERROR); - } - } - - if (info->m_State == STATE_CANCELING) { - setState(info, STATE_CANCELED); - } else if (info->m_State == STATE_PAUSING) { - if (info->m_Output.isOpen() && info->m_HasData) { - info->m_Output.write(info->m_Reply->readAll()); - } - setState(info, STATE_PAUSED); - } - - if (info->m_State == STATE_CANCELED || (info->m_Tries == 0 && error)) { - emit aboutToUpdate(); - info->m_Output.remove(); - delete info; - m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index); - if (error) - emit showMessage(tr("We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers.")); - emit update(-1); - } else if (info->isPausedState() || info->m_State == STATE_PAUSING) { - info->m_Output.close(); - createMetaFile(info); - emit update(index); - } else { - QString url = info->m_Urls[info->m_CurrentUrl]; - if (info->m_FileInfo->userData.contains("downloadMap")) { - foreach (const QVariant &server, info->m_FileInfo->userData["downloadMap"].toList()) { - QVariantMap serverMap = server.toMap(); - if (serverMap["URI"].toString() == url) { - int deltaTime = info->m_StartTime.secsTo(QTime::currentTime()); - if (deltaTime > 5) { - emit downloadSpeed(serverMap["Name"].toString(), (info->m_TotalSize - info->m_PreResumeSize) / deltaTime); - } // no division by zero please! Also, if the download is shorter than a few seconds, the result is way to inprecise - break; - } - } - } - - bool isNexus = info->m_FileInfo->repository == "Nexus"; - // need to change state before changing the file name, otherwise .unfinished is appended - if (isNexus) { - setState(info, STATE_FETCHINGMODINFO); - } else { - setState(info, STATE_NOFETCH); - } - - QString newName = getFileNameFromNetworkReply(reply); - QString oldName = QFileInfo(info->m_Output).fileName(); - - startDisableDirWatcher(); - if (!newName.isEmpty() && (oldName.isEmpty())) { - info->setName(getDownloadFileName(newName), true); - } else { - info->setName(m_OutputDirectory + "/" + info->m_FileName, true); // don't rename but remove the ".unfinished" extension - } - endDisableDirWatcher(); - - if (!isNexus) { - setState(info, STATE_READY); - } - - emit update(index); - } - reply->close(); - reply->deleteLater(); - - if ((info->m_Tries > 0) && error) { - --info->m_Tries; - resumeDownloadInt(index); - } - } else { - qWarning("no download index %d", index); - } -} - - -void DownloadManager::downloadError(QNetworkReply::NetworkError error) -{ - if (error != QNetworkReply::OperationCanceledError) { - QNetworkReply *reply = qobject_cast(sender()); - qWarning("%s (%d)", reply != nullptr ? qUtf8Printable(reply->errorString()) - : "Download error occured", - error); - } -} - - -void DownloadManager::metaDataChanged() -{ - int index = 0; - - DownloadInfo *info = findDownload(this->sender(), &index); - if (info != nullptr) { - QString newName = getFileNameFromNetworkReply(info->m_Reply); - if (!newName.isEmpty() && (info->m_FileName.isEmpty())) { - startDisableDirWatcher(); - info->setName(getDownloadFileName(newName), true); - endDisableDirWatcher(); - refreshAlphabeticalTranslation(); - if (!info->m_Output.isOpen() && !info->m_Output.open(QIODevice::WriteOnly | QIODevice::Append)) { - reportError(tr("failed to re-open %1").arg(info->m_FileName)); - setState(info, STATE_CANCELING); - } - } - } else { - qWarning("meta data event for unknown download"); - } -} - -void DownloadManager::directoryChanged(const QString&) -{ - if(DownloadManager::m_DirWatcherDisabler==0) - refreshList(); -} - -void DownloadManager::managedGameChanged(MOBase::IPluginGame const *managedGame) -{ - m_ManagedGame = managedGame; -} - -void DownloadManager::checkDownloadTimeout() -{ - for (int i = 0; i < m_ActiveDownloads.size(); ++i) { - if (m_ActiveDownloads[i]->m_StartTime.elapsed() - std::get<3>(m_ActiveDownloads[i]->m_SpeedDiff) > 5 * 1000 && - m_ActiveDownloads[i]->m_State == STATE_DOWNLOADING && m_ActiveDownloads[i]->m_Reply != nullptr && - m_ActiveDownloads[i]->m_Reply->isOpen()) { - pauseDownload(i); - downloadFinished(i); - resumeDownload(i); - } - } -} - -void DownloadManager::writeData(DownloadInfo *info) -{ - if (info != nullptr) { - qint64 ret = info->m_Output.write(info->m_Reply->readAll()); - if (ret < info->m_Reply->size()) { - QString fileName = info->m_FileName; // m_FileName may be destroyed after setState - setState(info, DownloadState::STATE_CANCELED); - qCritical(QString("Unable to write download \"%2\" to drive (return %1)").arg(ret).arg(info->m_FileName).toLocal8Bit()); - reportError(tr("Unable to write download to drive (return %1).\n" - "Check the drive's available storage.\n\n" - "Canceling download \"%2\"...").arg(ret).arg(fileName)); - } - } -} +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "downloadmanager.h" + +#include "nxmurl.h" +#include "nexusinterface.h" +#include "nxmaccessmanager.h" +#include "iplugingame.h" +#include "downloadmanager.h" +#include +#include +#include "utility.h" +#include "selectiondialog.h" +#include "bbcode.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + + +using namespace MOBase; + + +// TODO limit number of downloads, also display download during nxm requests, store modid/fileid with downloads + + +static const char UNFINISHED[] = ".unfinished"; + +unsigned int DownloadManager::DownloadInfo::s_NextDownloadID = 1U; +int DownloadManager::m_DirWatcherDisabler = 0; + + +DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createNew(const ModRepositoryFileInfo *fileInfo, const QStringList &URLs) +{ + DownloadInfo *info = new DownloadInfo; + info->m_DownloadID = s_NextDownloadID++; + info->m_StartTime.start(); + info->m_PreResumeSize = 0LL; + info->m_Progress = std::make_pair(0, "0.0 B/s "); + info->m_ResumePos = 0; + info->m_FileInfo = new ModRepositoryFileInfo(*fileInfo); + info->m_Urls = URLs; + info->m_CurrentUrl = 0; + info->m_Tries = AUTOMATIC_RETRIES; + info->m_State = STATE_STARTED; + info->m_TaskProgressId = TaskProgressManager::instance().getId(); + info->m_Reply = nullptr; + + return info; +} + +DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createFromMeta(const QString &filePath, bool showHidden, const QString outputDirectory) +{ + DownloadInfo *info = new DownloadInfo; + + QString metaFileName = filePath + ".meta"; + QFileInfo metaFileInfo(metaFileName); + if (QDir::fromNativeSeparators(metaFileInfo.path()).compare(QDir::fromNativeSeparators(outputDirectory), Qt::CaseInsensitive) != 0) return nullptr; + QSettings metaFile(metaFileName, QSettings::IniFormat); + if (!showHidden && metaFile.value("removed", false).toBool()) { + return nullptr; + } else { + info->m_Hidden = metaFile.value("removed", false).toBool(); + } + + QString fileName = QFileInfo(filePath).fileName(); + + if (fileName.endsWith(UNFINISHED)) { + info->m_FileName = fileName.mid( + 0, fileName.length() - static_cast(strlen(UNFINISHED))); + info->m_State = STATE_PAUSED; + } else { + info->m_FileName = fileName; + + if (metaFile.value("paused", false).toBool()) { + info->m_State = STATE_PAUSED; + } else if (metaFile.value("uninstalled", false).toBool()) { + info->m_State = STATE_UNINSTALLED; + } else if (metaFile.value("installed", false).toBool()) { + info->m_State = STATE_INSTALLED; + } else { + info->m_State = STATE_READY; + } + } + + info->m_DownloadID = s_NextDownloadID++; + info->m_Output.setFileName(filePath); + info->m_TotalSize = QFileInfo(filePath).size(); + info->m_PreResumeSize = info->m_TotalSize; + info->m_CurrentUrl = 0; + info->m_Urls = metaFile.value("url", "").toString().split(";"); + info->m_Tries = 0; + info->m_TaskProgressId = TaskProgressManager::instance().getId(); + QString gameName = metaFile.value("gameName", "").toString(); + int modID = metaFile.value("modID", 0).toInt(); + int fileID = metaFile.value("fileID", 0).toInt(); + info->m_FileInfo = new ModRepositoryFileInfo(gameName, modID, fileID); + info->m_FileInfo->name = metaFile.value("name", "").toString(); + if (info->m_FileInfo->name == "0") { + // bug in earlier version + info->m_FileInfo->name = ""; + } + info->m_FileInfo->modName = metaFile.value("modName", "").toString(); + info->m_FileInfo->gameName = gameName; + info->m_FileInfo->modID = modID; + info->m_FileInfo->fileID = fileID; + info->m_FileInfo->description = metaFile.value("description").toString(); + info->m_FileInfo->version.parse(metaFile.value("version", "0").toString()); + info->m_FileInfo->newestVersion.parse(metaFile.value("newestVersion", "0").toString()); + info->m_FileInfo->categoryID = metaFile.value("category", 0).toInt(); + info->m_FileInfo->fileCategory = metaFile.value("fileCategory", 0).toInt(); + info->m_FileInfo->repository = metaFile.value("repository", "Nexus").toString(); + info->m_FileInfo->userData = metaFile.value("userData").toMap(); + info->m_Reply = nullptr; + + return info; +} + +void DownloadManager::startDisableDirWatcher() +{ + DownloadManager::m_DirWatcherDisabler++; +} + + +void DownloadManager::endDisableDirWatcher() +{ + if (DownloadManager::m_DirWatcherDisabler > 0) + { + if (DownloadManager::m_DirWatcherDisabler == 1) + QCoreApplication::processEvents(); + DownloadManager::m_DirWatcherDisabler--; + } + else { + DownloadManager::m_DirWatcherDisabler = 0; + } +} + +void DownloadManager::DownloadInfo::setName(QString newName, bool renameFile) +{ + QString oldMetaFileName = QString("%1.meta").arg(m_FileName); + m_FileName = QFileInfo(newName).fileName(); + if ((m_State == DownloadManager::STATE_STARTED) || + (m_State == DownloadManager::STATE_DOWNLOADING) || + (m_State == DownloadManager::STATE_PAUSED)) { + newName.append(UNFINISHED); + oldMetaFileName = QString("%1%2.meta").arg(m_FileName).arg(UNFINISHED); + } + if (renameFile) { + if ((newName != m_Output.fileName()) && !m_Output.rename(newName)) { + reportError(tr("failed to rename \"%1\" to \"%2\"").arg(m_Output.fileName()).arg(newName)); + return; + } + + QFile metaFile(QFileInfo(newName).path() + "/" + oldMetaFileName); + if (metaFile.exists()) + metaFile.rename(newName.mid(0).append(".meta")); + } + if (!m_Output.isOpen()) { + // can't set file name if it's open + m_Output.setFileName(newName); + } +} + +bool DownloadManager::DownloadInfo::isPausedState() +{ + return m_State == STATE_PAUSED || m_State == STATE_ERROR; +} + +QString DownloadManager::DownloadInfo::currentURL() +{ + return m_Urls[m_CurrentUrl]; +} + + +DownloadManager::DownloadManager(NexusInterface *nexusInterface, QObject *parent) + : IDownloadManager(parent), m_NexusInterface(nexusInterface), m_DirWatcher(), m_ShowHidden(false) +{ + connect(&m_DirWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); + m_TimeoutTimer.setSingleShot(false); + //connect(&m_TimeoutTimer, SIGNAL(timeout()), this, SLOT(checkDownloadTimeout())); + m_TimeoutTimer.start(5 * 1000); +} + + +DownloadManager::~DownloadManager() +{ + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { + delete *iter; + } + m_ActiveDownloads.clear(); +} + + +bool DownloadManager::downloadsInProgress() +{ + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { + if ((*iter)->m_State < STATE_READY) { + return true; + } + } + return false; +} + +bool DownloadManager::downloadsInProgressNoPause() +{ + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { + if ((*iter)->m_State < STATE_READY && (*iter)->m_State != STATE_PAUSED) { + return true; + } + } + return false; +} + + +void DownloadManager::pauseAll() +{ + + // first loop: pause all downloads + for (int i = 0; i < m_ActiveDownloads.count(); ++i) { + if (m_ActiveDownloads[i]->m_State < STATE_READY) { + pauseDownload(i); + } + } + + ::Sleep(100); + + bool done = false; + QTime startTime = QTime::currentTime(); + // further loops: busy waiting for all downloads to complete. This could be neater... + while (!done && (startTime.secsTo(QTime::currentTime()) < 5)) { + QCoreApplication::processEvents(); + done = true; + foreach (DownloadInfo *info, m_ActiveDownloads) { + if ((info->m_State < STATE_CANCELED) || + (info->m_State == STATE_FETCHINGFILEINFO) || + (info->m_State == STATE_FETCHINGMODINFO)) { + done = false; + break; + } + } + if (!done) { + ::Sleep(100); + } + } + +} + + +void DownloadManager::setOutputDirectory(const QString &outputDirectory) +{ + QStringList directories = m_DirWatcher.directories(); + if (directories.length() != 0) { + m_DirWatcher.removePaths(directories); + } + m_OutputDirectory = QDir::fromNativeSeparators(outputDirectory); + refreshList(); + m_DirWatcher.addPath(m_OutputDirectory); +} + + +void DownloadManager::setPreferredServers(const std::map &preferredServers) +{ + m_PreferredServers = preferredServers; +} + + +void DownloadManager::setSupportedExtensions(const QStringList &extensions) +{ + m_SupportedExtensions = extensions; + refreshList(); +} + +void DownloadManager::setShowHidden(bool showHidden) +{ + m_ShowHidden = showHidden; + refreshList(); +} + +void DownloadManager::setPluginContainer(PluginContainer *pluginContainer) +{ + m_NexusInterface->setPluginContainer(pluginContainer); +} + + + + + + +void DownloadManager::refreshList() +{ + try { + //avoid triggering other refreshes + startDisableDirWatcher(); + + int downloadsBefore = m_ActiveDownloads.size(); + + // remove finished downloads + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end();) { + if (((*iter)->m_State == STATE_READY) || ((*iter)->m_State == STATE_INSTALLED) || ((*iter)->m_State == STATE_UNINSTALLED)) { + delete *iter; + iter = m_ActiveDownloads.erase(iter); + } else { + ++iter; + } + } + + QStringList nameFilters(m_SupportedExtensions); + foreach (const QString &extension, m_SupportedExtensions) { + nameFilters.append("*." + extension); + } + + nameFilters.append(QString("*").append(UNFINISHED)); + QDir dir(QDir::fromNativeSeparators(m_OutputDirectory)); + + // find orphaned meta files and delete them (sounds cruel but it's better for everyone) + QStringList orphans; + QStringList metaFiles = dir.entryList(QStringList() << "*.meta"); + foreach (const QString &metaFile, metaFiles) { + QString baseFile = metaFile.left(metaFile.length() - 5); + if (!QFile::exists(dir.absoluteFilePath(baseFile))) { + orphans.append(dir.absoluteFilePath(metaFile)); + } + } + if (orphans.size() > 0) { + qDebug("%d orphaned meta files will be deleted", orphans.size()); + shellDelete(orphans, true); + } + + // add existing downloads to list + foreach (QString file, dir.entryList(nameFilters, QDir::Files, QDir::Time)) { + bool Exists = false; + for (QVector::const_iterator Iter = m_ActiveDownloads.begin(); Iter != m_ActiveDownloads.end() && !Exists; ++Iter) { + if (QString::compare((*Iter)->m_FileName, file, Qt::CaseInsensitive) == 0) { + Exists = true; + } else if (QString::compare(QFileInfo((*Iter)->m_Output.fileName()).fileName(), file, Qt::CaseInsensitive) == 0) { + Exists = true; + } + } + if (Exists) { + continue; + } + + QString fileName = QDir::fromNativeSeparators(m_OutputDirectory) + "/" + file; + + DownloadInfo *info = DownloadInfo::createFromMeta(fileName, m_ShowHidden, m_OutputDirectory); + if (info != nullptr) { + m_ActiveDownloads.push_front(info); + } + } + + //if (m_ActiveDownloads.size() != downloadsBefore) { + qDebug("Downloads after refresh: %d", m_ActiveDownloads.size()); + //} + emit update(-1); + + //let watcher trigger refreshes again + endDisableDirWatcher(); + + } catch (const std::bad_alloc&) { + reportError(tr("Memory allocation error (in refreshing directory).")); + } +} + + +bool DownloadManager::addDownload(const QStringList &URLs, QString gameName, + int modID, int fileID, const ModRepositoryFileInfo *fileInfo) +{ + QString fileName = QFileInfo(URLs.first()).fileName(); + if (fileName.isEmpty()) { + fileName = "unknown"; + } + + QUrl preferredUrl = QUrl::fromEncoded(URLs.first().toLocal8Bit()); + qDebug("selected download url: %s", qUtf8Printable(preferredUrl.toString())); + QNetworkRequest request(preferredUrl); + request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent()); + return addDownload(m_NexusInterface->getAccessManager()->get(request), URLs, fileName, gameName, modID, fileID, fileInfo); +} + + +bool DownloadManager::addDownload(QNetworkReply *reply, const ModRepositoryFileInfo *fileInfo) +{ + QString fileName = getFileNameFromNetworkReply(reply); + if (fileName.isEmpty()) { + fileName = "unknown"; + } + + return addDownload(reply, QStringList(reply->url().toString()), fileName, fileInfo->gameName, fileInfo->modID, fileInfo->fileID, fileInfo); +} + + +bool DownloadManager::addDownload(QNetworkReply *reply, const QStringList &URLs, const QString &fileName, + QString gameName, int modID, int fileID, const ModRepositoryFileInfo *fileInfo) +{ + // download invoked from an already open network reply (i.e. download link in the browser) + DownloadInfo *newDownload = DownloadInfo::createNew(fileInfo, URLs); + + QString baseName = fileName; + if (!fileInfo->fileName.isEmpty()) { + baseName = fileInfo->fileName; + } else { + QString dispoName = getFileNameFromNetworkReply(reply); + + if (!dispoName.isEmpty()) { + baseName = dispoName; + } + } + + startDisableDirWatcher(); + newDownload->setName(getDownloadFileName(baseName), false); + endDisableDirWatcher(); + + startDownload(reply, newDownload, false); +// emit update(-1); + return true; +} + + +void DownloadManager::removePending(QString gameName, int modID, int fileID) +{ + emit aboutToUpdate(); + for (auto iter : m_PendingDownloads) { + if (gameName.compare(std::get<0>(iter), Qt::CaseInsensitive) == 0 && (std::get<1>(iter) == modID) && (std::get<2>(iter) == fileID)) { + m_PendingDownloads.removeAt(m_PendingDownloads.indexOf(iter)); + break; + } + } + emit update(-1); +} + + +void DownloadManager::startDownload(QNetworkReply *reply, DownloadInfo *newDownload, bool resume) +{ + reply->setReadBufferSize(1024 * 1024); // don't read more than 1MB at once to avoid memory troubles + newDownload->m_Reply = reply; + setState(newDownload, STATE_DOWNLOADING); + if (newDownload->m_Urls.count() == 0) { + newDownload->m_Urls = QStringList(reply->url().toString()); + } + + QIODevice::OpenMode mode = QIODevice::WriteOnly; + if (resume) { + mode |= QIODevice::Append; + } + + newDownload->m_StartTime.start(); + createMetaFile(newDownload); + + if (!newDownload->m_Output.open(mode)) { + reportError(tr("failed to download %1: could not open output file: %2") + .arg(reply->url().toString()).arg(newDownload->m_Output.fileName())); + return; + } + + connect(newDownload->m_Reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(downloadProgress(qint64, qint64))); + connect(newDownload->m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(newDownload->m_Reply, SIGNAL(readyRead()), this, SLOT(downloadReadyRead())); + connect(newDownload->m_Reply, SIGNAL(metaDataChanged()), this, SLOT(metaDataChanged())); + + if (!resume) { + newDownload->m_PreResumeSize = newDownload->m_Output.size(); + removePending(newDownload->m_FileInfo->gameName, newDownload->m_FileInfo->modID, newDownload->m_FileInfo->fileID); + + emit aboutToUpdate(); + m_ActiveDownloads.append(newDownload); + + emit update(-1); + emit downloadAdded(); + + if (QFile::exists(m_OutputDirectory + "/" + newDownload->m_FileName)) { + setState(newDownload, STATE_PAUSING); + QCoreApplication::processEvents(); + if (QMessageBox::question(nullptr, tr("Download again?"), tr("A file with the same name \"%1\" has already been downloaded. " + "Do you want to download it again? The new file will receive a different name.").arg(newDownload->m_FileName), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { + if (reply->isFinished()) + setState(newDownload, STATE_CANCELED); + else + setState(newDownload, STATE_CANCELING); + } else { + startDisableDirWatcher(); + newDownload->setName(getDownloadFileName(newDownload->m_FileName, true), true); + endDisableDirWatcher(); + if (newDownload->m_State == STATE_PAUSED) + resumeDownload(indexByName(newDownload->m_FileName)); + else + setState(newDownload, STATE_DOWNLOADING); + } + } else + connect(newDownload->m_Reply, SIGNAL(finished()), this, SLOT(downloadFinished())); + + + QCoreApplication::processEvents(); + + if (newDownload->m_State != STATE_DOWNLOADING && + newDownload->m_State != STATE_READY && + newDownload->m_State != STATE_FETCHINGMODINFO && + reply->isFinished()) { + downloadFinished(indexByName(newDownload->m_FileName)); + return; + } + } else + connect(newDownload->m_Reply, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + + +void DownloadManager::addNXMDownload(const QString &url) +{ + NXMUrl nxmInfo(url); + + QStringList validGames; + validGames.append(m_ManagedGame->gameShortName()); + validGames.append(m_ManagedGame->validShortNames()); + qDebug("add nxm download: %s", qUtf8Printable(url)); + if (!validGames.contains(nxmInfo.game(), Qt::CaseInsensitive)) { + qDebug("download requested for wrong game (game: %s, url: %s)", qUtf8Printable(m_ManagedGame->gameShortName()), qUtf8Printable(nxmInfo.game())); + QMessageBox::information(nullptr, tr("Wrong Game"), tr("The download link is for a mod for \"%1\" but this instance of MO " + "has been set up for \"%2\".").arg(nxmInfo.game()).arg(m_ManagedGame->gameShortName()), QMessageBox::Ok); + return; + } + + for (auto tuple : m_PendingDownloads) { + if (std::get<0>(tuple).compare(nxmInfo.game(), Qt::CaseInsensitive) == 0, std::get<1>(tuple) == nxmInfo.modId() && std::get<2>(tuple) == nxmInfo.fileId()) { + qDebug("download requested is already started (mod id: %s, file id: %s)", qUtf8Printable(QString(nxmInfo.modId())), qUtf8Printable(QString(nxmInfo.fileId()))); + QMessageBox::information(nullptr, tr("Already Started"), tr("A download for this mod file has already been queued."), QMessageBox::Ok); + return; + } + } + + for (DownloadInfo *download : m_ActiveDownloads) { + if (download->m_FileInfo->modID == nxmInfo.modId() && download->m_FileInfo->fileID == nxmInfo.fileId()) { + if (download->m_State == STATE_DOWNLOADING || download->m_State == STATE_PAUSED || download->m_State == STATE_STARTED) { + qDebug("download requested is already started (mod: %s, file: %s)", qUtf8Printable(QString(download->m_FileInfo->modID)), + qUtf8Printable(download->m_FileInfo->fileName)); + + QMessageBox::information(nullptr, tr("Already Started"), tr("There is already a download started for this file (mod: %1, file: %2).") + .arg(download->m_FileInfo->modName).arg(download->m_FileInfo->fileName), QMessageBox::Ok); + return; + } + } + } + + emit aboutToUpdate(); + + m_PendingDownloads.append(std::make_tuple(nxmInfo.game(), nxmInfo.modId(), nxmInfo.fileId())); + + emit update(-1); + emit downloadAdded(); + m_RequestIDs.insert(m_NexusInterface->requestFileInfo(nxmInfo.game(), nxmInfo.modId(), nxmInfo.fileId(), this, nxmInfo.fileId(), "")); +} + + +void DownloadManager::removeFile(int index, bool deleteFile) +{ + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + + if (index >= m_ActiveDownloads.size()) { + throw MyException(tr("remove: invalid download index %1").arg(index)); + } + + DownloadInfo *download = m_ActiveDownloads.at(index); + QString filePath = m_OutputDirectory + "/" + download->m_FileName; + if ((download->m_State == STATE_STARTED) || + (download->m_State == STATE_DOWNLOADING)) { + // shouldn't have been possible + qCritical("tried to remove active download"); + endDisableDirWatcher(); + return; + } + + if ((download->m_State == STATE_PAUSED) || (download->m_State == STATE_ERROR)) { + filePath = download->m_Output.fileName(); + } + + if (deleteFile) { + if (!shellDelete(QStringList(filePath), true)) { + reportError(tr("failed to delete %1").arg(filePath)); + endDisableDirWatcher(); + return; + } + + QFile metaFile(filePath.append(".meta")); + if (metaFile.exists() && !shellDelete(QStringList(filePath), true)) { + reportError(tr("failed to delete meta file for %1").arg(filePath)); + } + } else { + QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat); + if(!download->m_Hidden) + metaSettings.setValue("removed", true); + } + + endDisableDirWatcher(); +} + +class LessThanWrapper +{ +public: + LessThanWrapper(DownloadManager *manager) : m_Manager(manager) {} + bool operator()(int LHS, int RHS) { + return m_Manager->getFileName(LHS).compare(m_Manager->getFileName(RHS), Qt::CaseInsensitive) < 0; + + } + +private: + DownloadManager *m_Manager; +}; + + +bool DownloadManager::ByName(int LHS, int RHS) +{ + return m_ActiveDownloads[LHS]->m_FileName < m_ActiveDownloads[RHS]->m_FileName; +} + + +void DownloadManager::refreshAlphabeticalTranslation() +{ + m_AlphabeticalTranslation.clear(); + int pos = 0; + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++pos) { + m_AlphabeticalTranslation.push_back(pos); + } + + qSort(m_AlphabeticalTranslation.begin(), m_AlphabeticalTranslation.end(), LessThanWrapper(this)); +} + + +void DownloadManager::restoreDownload(int index) +{ + + if (index < 0) { + DownloadState minState = STATE_READY ; + index = 0; + + for (QVector::const_iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter ) { + + if ((*iter)->m_State >= minState) { + restoreDownload(index); + } + index++; + } + } + else { + if (index >= m_ActiveDownloads.size()) { + throw MyException(tr("restore: invalid download index: %1").arg(index)); + } + + DownloadInfo *download = m_ActiveDownloads.at(index); + if (download->m_Hidden) { + download->m_Hidden = false; + + QString filePath = m_OutputDirectory + "/" + download->m_FileName; + + //avoid dirWatcher triggering refreshes + startDisableDirWatcher(); + QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat); + metaSettings.setValue("removed", false); + + endDisableDirWatcher(); + } + } +} + + +void DownloadManager::removeDownload(int index, bool deleteFile) +{ + try { + //avoid dirWatcher triggering refreshes + startDisableDirWatcher(); + + emit aboutToUpdate(); + + if (index < 0) { + bool removeAll = (index == -1); + DownloadState removeState = (index == -2 ? STATE_INSTALLED : STATE_UNINSTALLED); + + index = 0; + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end();) { + DownloadState downloadState = (*iter)->m_State; + if ((removeAll && (downloadState >= STATE_READY)) || + (removeState == downloadState)) { + removeFile(index, deleteFile); + delete *iter; + iter = m_ActiveDownloads.erase(iter); + } else { + ++iter; + ++index; + } + } + } else { + if (index >= m_ActiveDownloads.size()) { + reportError(tr("remove: invalid download index %1").arg(index)); + //emit update(-1); + endDisableDirWatcher(); + return; + } + + removeFile(index, deleteFile); + delete m_ActiveDownloads.at(index); + m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index); + } + emit update(-1); + endDisableDirWatcher(); + } catch (const std::exception &e) { + qCritical("failed to remove download: %s", e.what()); + } + refreshList(); +} + + +void DownloadManager::cancelDownload(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("cancel: invalid download index %1").arg(index)); + return; + } + + if (m_ActiveDownloads.at(index)->m_State == STATE_DOWNLOADING) { + setState(m_ActiveDownloads.at(index), STATE_CANCELING); + } +} + + +void DownloadManager::pauseDownload(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("pause: invalid download index %1").arg(index)); + return; + } + + DownloadInfo *info = m_ActiveDownloads.at(index); + + if (info->m_State == STATE_DOWNLOADING) { + if ((info->m_Reply != nullptr) && (info->m_Reply->isRunning())) { + setState(info, STATE_PAUSING); + } else { + setState(info, STATE_PAUSED); + } + } else if ((info->m_State == STATE_FETCHINGMODINFO) || (info->m_State == STATE_FETCHINGFILEINFO)) { + setState(info, STATE_READY); + } +} + +void DownloadManager::resumeDownload(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("resume: invalid download index %1").arg(index)); + return; + } + DownloadInfo *info = m_ActiveDownloads[index]; + info->m_Tries = AUTOMATIC_RETRIES; + resumeDownloadInt(index); +} + +void DownloadManager::resumeDownloadInt(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("resume (int): invalid download index %1").arg(index)); + return; + } + DownloadInfo *info = m_ActiveDownloads[index]; + + // Check for finished download; + if (info->m_TotalSize <= info->m_Output.size() && info->m_Reply != nullptr + && info->m_Reply->isOpen() && info->m_Reply->isFinished() && info->m_State != STATE_ERROR) { + setState(info, STATE_DOWNLOADING); + downloadFinished(index); + return; + } + + if (info->isPausedState() || info->m_State == STATE_PAUSING) { + if (info->m_State == STATE_PAUSING) { + if (info->m_Output.isOpen()) { + writeData(info); + if (info->m_State == STATE_PAUSING) { + setState(info, STATE_PAUSED); + } + } + } + if ((info->m_Urls.size() == 0) + || ((info->m_Urls.size() == 1) && (info->m_Urls[0].size() == 0))) { + emit showMessage(tr("No known download urls. Sorry, this download can't be resumed.")); + return; + } + if (info->m_State == STATE_ERROR) { + info->m_CurrentUrl = (info->m_CurrentUrl + 1) % info->m_Urls.count(); + } + qDebug("request resume from url %s", qUtf8Printable(info->currentURL())); + QNetworkRequest request(QUrl::fromEncoded(info->currentURL().toLocal8Bit())); + request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent()); + if (info->m_State != STATE_ERROR) { + info->m_ResumePos = info->m_Output.size(); + QByteArray rangeHeader = "bytes=" + QByteArray::number(info->m_ResumePos) + "-"; + request.setRawHeader("Range", rangeHeader); + } + std::get<0>(info->m_SpeedDiff) = 0; + std::get<1>(info->m_SpeedDiff) = 0; + std::get<2>(info->m_SpeedDiff) = 0; + std::get<3>(info->m_SpeedDiff) = 0; + std::get<4>(info->m_SpeedDiff) = 0; + qDebug("resume at %lld bytes", info->m_ResumePos); + startDownload(m_NexusInterface->getAccessManager()->get(request), info, true); + } + emit update(index); +} + + +DownloadManager::DownloadInfo *DownloadManager::downloadInfoByID(unsigned int id) +{ + auto iter = std::find_if(m_ActiveDownloads.begin(), m_ActiveDownloads.end(), + [id](DownloadInfo *info) { return info->m_DownloadID == id; }); + if (iter != m_ActiveDownloads.end()) { + return *iter; + } else { + return nullptr; + } +} + + +void DownloadManager::queryInfo(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("query: invalid download index %1").arg(index)); + return; + } + DownloadInfo *info = m_ActiveDownloads[index]; + + if (info->m_FileInfo->repository != "Nexus") { + qWarning("re-querying file info is currently only possible with Nexus"); + return; + } + + if (info->m_State < DownloadManager::STATE_READY) { + // UI shouldn't allow this + return; + } + + if (info->m_FileInfo->modID <= 0) { + QString fileName = getFileName(index); + QString ignore; + NexusInterface::interpretNexusFileName(fileName, ignore, info->m_FileInfo->modID, true); + if (info->m_FileInfo->modID < 0) { + bool ok = false; + int modId = QInputDialog::getInt( + nullptr, tr("Please enter the nexus mod id"), tr("Mod ID:"), 1, 1, + std::numeric_limits::max(), 1, &ok); + // careful now: while the dialog was displayed, events were processed. + // the download list might have changed and our info-ptr invalidated. + if (ok) + m_ActiveDownloads[index]->m_FileInfo->modID = modId; + return; + } + } + + if (info->m_FileInfo->gameName.size() == 0) { + SelectionDialog selection(tr("Please select the source game code for %1").arg(getFileName(index))); + + std::vector> choices = m_NexusInterface->getGameChoices(m_ManagedGame); + for (auto choice : choices) { + selection.addChoice(choice.first, choice.second, choice.first); + } + if (selection.exec() == QDialog::Accepted) { + info->m_FileInfo->gameName = selection.getChoiceData().toString(); + } else { + info->m_FileInfo->gameName = m_ManagedGame->gameShortName(); + } + } + info->m_ReQueried = true; + setState(info, STATE_FETCHINGMODINFO); +} + +void DownloadManager::visitOnNexus(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("VisitNexus: invalid download index %1").arg(index)); + return; + } + DownloadInfo *info = m_ActiveDownloads[index]; + + if (info->m_FileInfo->repository != "Nexus") { + qWarning("Visiting mod page is currently only possible with Nexus"); + return; + } + + if (info->m_State < DownloadManager::STATE_READY) { + // UI shouldn't allow this + return; + } + int modID = info->m_FileInfo->modID; + + QString gameName = info->m_FileInfo->gameName; + if (modID > 0) { + QDesktopServices::openUrl(QUrl(m_NexusInterface->getModURL(modID, gameName))); + } + else { + emit showMessage(tr("Nexus ID for this Mod is unknown")); + } +} + +void DownloadManager::openFile(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("OpenFile: invalid download index %1").arg(index)); + return; + } + QDir path = QDir(m_OutputDirectory); + if (path.exists(getFileName(index))) { + + ::ShellExecuteW(nullptr, L"open", ToWString(QDir::toNativeSeparators(getFilePath(index))).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + return; + } + + ::ShellExecuteW(nullptr, L"explore", ToWString(QDir::toNativeSeparators(m_OutputDirectory)).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + return; +} + +void DownloadManager::openInDownloadsFolder(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("OpenFileInDownloadsFolder: invalid download index %1").arg(index)); + return; + } + QString params = "/select,\""; + QDir path = QDir(m_OutputDirectory); + if (path.exists(getFileName(index))) { + params = params + QDir::toNativeSeparators(getFilePath(index)) + "\""; + + ::ShellExecuteW(nullptr, nullptr, L"explorer", ToWString(params).c_str(), nullptr, SW_SHOWNORMAL); + return; + } + else if (path.exists(getFileName(index) + ".unfinished")) { + params = params + QDir::toNativeSeparators(getFilePath(index)+".unfinished") + "\""; + + ::ShellExecuteW(nullptr, nullptr, L"explorer", ToWString(params).c_str(), nullptr, SW_SHOWNORMAL); + return; + } + + ::ShellExecuteW(nullptr, L"explore", ToWString(QDir::toNativeSeparators(m_OutputDirectory)).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + return; +} + + +int DownloadManager::numTotalDownloads() const +{ + return m_ActiveDownloads.size(); +} + +int DownloadManager::numPendingDownloads() const +{ + return m_PendingDownloads.size(); +} + +std::tuple DownloadManager::getPendingDownload(int index) +{ + if ((index < 0) || (index >= m_PendingDownloads.size())) { + throw MyException(tr("get pending: invalid download index %1").arg(index)); + } + + return m_PendingDownloads.at(index); +} + +QString DownloadManager::getFilePath(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("get path: invalid download index %1").arg(index)); + } + + return m_OutputDirectory + "/" + m_ActiveDownloads.at(index)->m_FileName; +} + +QString DownloadManager::getFileTypeString(int fileType) +{ + switch (fileType) { + case 1: return tr("Main"); + case 2: return tr("Update"); + case 3: return tr("Optional"); + case 4: return tr("Old"); + case 5: return tr("Misc"); + default: return tr("Unknown"); + } +} + +QString DownloadManager::getDisplayName(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("display name: invalid download index %1").arg(index)); + } + + DownloadInfo *info = m_ActiveDownloads.at(index); + + QTextDocument doc; + if (!info->m_FileInfo->name.isEmpty()) { + doc.setHtml(info->m_FileInfo->name); + return QString("%1 (%2, v%3)").arg(doc.toPlainText()) + .arg(getFileTypeString(info->m_FileInfo->fileCategory)) + .arg(info->m_FileInfo->version.displayString()); + } else { + doc.setHtml(info->m_FileName); + return doc.toPlainText(); + } +} + +QString DownloadManager::getFileName(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("file name: invalid download index %1").arg(index)); + } + + return m_ActiveDownloads.at(index)->m_FileName; +} + +QDateTime DownloadManager::getFileTime(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("file time: invalid download index %1").arg(index)); + } + + DownloadInfo *info = m_ActiveDownloads.at(index); + if (!info->m_Created.isValid()) { + info->m_Created = QFileInfo(info->m_Output).created(); + } + + return info->m_Created; +} + +qint64 DownloadManager::getFileSize(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("file size: invalid download index %1").arg(index)); + } + + return m_ActiveDownloads.at(index)->m_TotalSize; +} + + +std::pair DownloadManager::getProgress(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("progress: invalid download index %1").arg(index)); + } + + return m_ActiveDownloads.at(index)->m_Progress; +} + + +DownloadManager::DownloadState DownloadManager::getState(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("state: invalid download index %1").arg(index)); + } + + return m_ActiveDownloads.at(index)->m_State; +} + + +bool DownloadManager::isInfoIncomplete(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("infocomplete: invalid download index %1").arg(index)); + } + + DownloadInfo *info = m_ActiveDownloads.at(index); + if (info->m_FileInfo->repository != "Nexus") { + // other repositories currently don't support re-querying info anyway + return false; + } + return (info->m_FileInfo->fileID == 0) || (info->m_FileInfo->modID == 0); +} + + +int DownloadManager::getModID(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("mod id: invalid download index %1").arg(index)); + } + return m_ActiveDownloads.at(index)->m_FileInfo->modID; +} + +QString DownloadManager::getGameName(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("mod id: invalid download index %1").arg(index)); + } + return m_ActiveDownloads.at(index)->m_FileInfo->gameName; +} + +bool DownloadManager::isHidden(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("ishidden: invalid download index %1").arg(index)); + } + return m_ActiveDownloads.at(index)->m_Hidden; +} + + +const ModRepositoryFileInfo *DownloadManager::getFileInfo(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("file info: invalid download index %1").arg(index)); + } + + return m_ActiveDownloads.at(index)->m_FileInfo; +} + + +void DownloadManager::markInstalled(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("mark installed: invalid download index %1").arg(index)); + } + + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + + DownloadInfo *info = m_ActiveDownloads.at(index); + QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); + metaFile.setValue("installed", true); + metaFile.setValue("uninstalled", false); + + endDisableDirWatcher(); + + setState(m_ActiveDownloads.at(index), STATE_INSTALLED); +} + +void DownloadManager::markInstalled(QString fileName) +{ + int index = indexByName(fileName); + if (index >= 0) { + markInstalled(index); + } else { + DownloadInfo *info = getDownloadInfo(fileName); + if (info != nullptr) { + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + + QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); + metaFile.setValue("installed", true); + metaFile.setValue("uninstalled", false); + delete info; + + endDisableDirWatcher(); + } + } +} + +DownloadManager::DownloadInfo* DownloadManager::getDownloadInfo(QString fileName) +{ + return DownloadInfo::createFromMeta(fileName, true, m_OutputDirectory); +} + +void DownloadManager::markUninstalled(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("mark uninstalled: invalid download index %1").arg(index)); + } + + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + + DownloadInfo *info = m_ActiveDownloads.at(index); + QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); + metaFile.setValue("uninstalled", true); + + endDisableDirWatcher(); + + setState(m_ActiveDownloads.at(index), STATE_UNINSTALLED); +} + + +void DownloadManager::markUninstalled(QString fileName) +{ + int index = indexByName(fileName); + if (index >= 0) { + markUninstalled(index); + } else { + QString filePath = QDir::fromNativeSeparators(m_OutputDirectory) + "/" + fileName; + DownloadInfo *info = getDownloadInfo(filePath); + if (info != nullptr) { + + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + + QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); + metaFile.setValue("uninstalled", true); + delete info; + + endDisableDirWatcher(); + } + } +} + + +QString DownloadManager::getDownloadFileName(const QString &baseName, bool rename) const +{ + QString fullPath = m_OutputDirectory + "/" + baseName; + if (QFile::exists(fullPath) && rename) { + int i = 1; + while (QFile::exists(QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName))) { + ++i; + } + + fullPath = QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName); + } + return fullPath; +} + + +QString DownloadManager::getFileNameFromNetworkReply(QNetworkReply *reply) +{ + if (reply->hasRawHeader("Content-Disposition")) { + std::regex exp("filename=\"(.*)\""); + + std::cmatch result; + if (std::regex_search(reply->rawHeader("Content-Disposition").constData(), result, exp)) { + return QString::fromUtf8(result.str(1).c_str()); + } + } + + return QString(); +} + + +void DownloadManager::setState(DownloadManager::DownloadInfo *info, DownloadManager::DownloadState state) +{ + int row = 0; + for (int i = 0; i < m_ActiveDownloads.size(); ++i) { + if (m_ActiveDownloads[i] == info) { + row = i; + break; + } + } + info->m_State = state; + switch (state) { + case STATE_PAUSED: + case STATE_ERROR: { + info->m_Reply->abort(); + info->m_Output.close(); + } break; + case STATE_CANCELED: { + info->m_Reply->abort(); + } break; + case STATE_FETCHINGMODINFO: { + m_RequestIDs.insert(m_NexusInterface->requestDescription(info->m_FileInfo->gameName, info->m_FileInfo->modID, this, info->m_DownloadID, QString())); + } break; + case STATE_FETCHINGFILEINFO: { + m_RequestIDs.insert(m_NexusInterface->requestFiles(info->m_FileInfo->gameName, info->m_FileInfo->modID, this, info->m_DownloadID, QString())); + } break; + case STATE_READY: { + createMetaFile(info); + emit downloadComplete(row); + } break; + default: /* NOP */ break; + } + emit stateChanged(row, state); +} + + +DownloadManager::DownloadInfo *DownloadManager::findDownload(QObject *reply, int *index) const +{ + // reverse search as newer, thus more relevant, downloads are at the end + for (int i = m_ActiveDownloads.size() - 1; i >= 0; --i) { + if (m_ActiveDownloads[i]->m_Reply == reply) { + if (index != nullptr) { + *index = i; + } + return m_ActiveDownloads[i]; + } + } + return nullptr; +} + + +void DownloadManager::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + if (bytesTotal == 0) { + return; + } + int index = 0; + try { + DownloadInfo *info = findDownload(this->sender(), &index); + if (info != nullptr) { + info->m_HasData = true; + if (info->m_State == STATE_CANCELING) { + setState(info, STATE_CANCELED); + } else if (info->m_State == STATE_PAUSING) { + setState(info, STATE_PAUSED); + } + else { + if (bytesTotal > info->m_TotalSize) { + info->m_TotalSize = bytesTotal; + } + int oldProgress = info->m_Progress.first; + info->m_Progress.first = ((info->m_ResumePos + bytesReceived) * 100) / (info->m_ResumePos + bytesTotal); + + int elapsed = info->m_StartTime.elapsed(); + std::get<0>(info->m_SpeedDiff) = bytesReceived - std::get<2>(info->m_SpeedDiff); + std::get<1>(info->m_SpeedDiff) = elapsed - std::get<3>(info->m_SpeedDiff); + std::get<2>(info->m_SpeedDiff) = bytesReceived; + std::get<3>(info->m_SpeedDiff) = elapsed; + + double calc = ((double)std::get<0>(info->m_SpeedDiff)) / (((double)(std::get<1>(info->m_SpeedDiff)) / 5000.0)); + std::get<4>(info->m_SpeedDiff) = ((calc*0.5) + (std::get<4>(info->m_SpeedDiff)*1.5)) / 2; + + // calculate the download speed + double speed = (std::get<4>(info->m_SpeedDiff) * 1000.0) / (5 * 1000); + + QString unit; + if (speed < 1000) { + unit = "B/s"; + } + else if (speed < 1000*1024) { + speed /= 1024; + unit = "KB/s"; + } + else { + speed /= 1024 * 1024; + unit = "MB/s"; + } + + info->m_Progress.second = QString::fromLatin1("%1% - %2 %3").arg(info->m_Progress.first).arg(QString::number(speed, 'f', 1)).arg(unit); + + TaskProgressManager::instance().updateProgress(info->m_TaskProgressId, bytesReceived, bytesTotal); + emit update(index); + } + } + } catch (const std::bad_alloc&) { + reportError(tr("Memory allocation error (in processing progress event).")); + } +} + + +void DownloadManager::downloadReadyRead() +{ + try { + writeData(findDownload(this->sender())); + } catch (const std::bad_alloc&) { + reportError(tr("Memory allocation error (in processing downloaded data).")); + } +} + + +void DownloadManager::createMetaFile(DownloadInfo *info) +{ + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + + QSettings metaFile(QString("%1.meta").arg(info->m_Output.fileName()), QSettings::IniFormat); + metaFile.setValue("gameName", info->m_FileInfo->gameName); + metaFile.setValue("modID", info->m_FileInfo->modID); + metaFile.setValue("fileID", info->m_FileInfo->fileID); + metaFile.setValue("url", info->m_Urls.join(";")); + metaFile.setValue("name", info->m_FileInfo->name); + metaFile.setValue("description", info->m_FileInfo->description); + metaFile.setValue("modName", info->m_FileInfo->modName); + metaFile.setValue("version", info->m_FileInfo->version.canonicalString()); + metaFile.setValue("newestVersion", info->m_FileInfo->newestVersion.canonicalString()); + metaFile.setValue("fileTime", info->m_FileInfo->fileTime); + metaFile.setValue("fileCategory", info->m_FileInfo->fileCategory); + metaFile.setValue("category", info->m_FileInfo->categoryID); + metaFile.setValue("repository", info->m_FileInfo->repository); + metaFile.setValue("userData", info->m_FileInfo->userData); + metaFile.setValue("installed", info->m_State == DownloadManager::STATE_INSTALLED); + metaFile.setValue("uninstalled", info->m_State == DownloadManager::STATE_UNINSTALLED); + metaFile.setValue("paused", (info->m_State == DownloadManager::STATE_PAUSED) || + (info->m_State == DownloadManager::STATE_ERROR)); + metaFile.setValue("removed", info->m_Hidden); + + endDisableDirWatcher(); + // slightly hackish... + for (int i = 0; i < m_ActiveDownloads.size(); ++i) { + if (m_ActiveDownloads[i] == info) { + emit update(i); + } + } +} + + +void DownloadManager::nxmDescriptionAvailable(QString, int, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + QVariantMap result = resultData.toMap(); + + DownloadInfo *info = downloadInfoByID(userData.toInt()); + if (info == nullptr) return; + info->m_FileInfo->categoryID = result["category_id"].toInt(); + QTextDocument doc; + doc.setHtml(result["name"].toString().trimmed()); + info->m_FileInfo->modName = doc.toPlainText(); + info->m_FileInfo->newestVersion.parse(result["version"].toString()); + if (info->m_FileInfo->fileID != 0) { + setState(info, STATE_READY); + } else { + setState(info, STATE_FETCHINGFILEINFO); + } +} + +void DownloadManager::nxmFilesAvailable(QString, int, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + DownloadInfo *info = downloadInfoByID(userData.toInt()); + if (info == nullptr) return; + + QVariantMap result = resultData.toMap(); + QVariantList files = result["files"].toList(); + + + // MO sometimes prepends _ to the filename in case of duplicate downloads. + // this may muck up the file name comparison + QString alternativeLocalName = info->m_FileName; + + QRegExp expression("^\\d_(.*)$"); + if (expression.indexIn(alternativeLocalName) == 0) { + alternativeLocalName = expression.cap(1); + } + + bool found = false; + + for (QVariant file : files) { + QVariantMap fileInfo = file.toMap(); + QString fileName = fileInfo["file_name"].toString(); + QString fileNameVariant = fileName.mid(0).replace(' ', '_'); + if ((fileName == info->m_FileName) || (fileName == alternativeLocalName) || + (fileNameVariant == info->m_FileName) || (fileNameVariant == alternativeLocalName)) { + info->m_FileInfo->name = fileInfo["name"].toString(); + info->m_FileInfo->version.parse(fileInfo["version"].toString()); + if (!info->m_FileInfo->version.isValid()) { + info->m_FileInfo->version = info->m_FileInfo->newestVersion; + } + info->m_FileInfo->fileCategory = fileInfo["category_id"].toInt(); + info->m_FileInfo->fileTime = QDateTime::fromMSecsSinceEpoch(fileInfo["uploaded_timestamp"].toLongLong()); + info->m_FileInfo->fileID = fileInfo["file_id"].toInt(); + info->m_FileInfo->fileName = fileInfo["file_name"].toString(); + info->m_FileInfo->description = BBCode::convertToHTML(fileInfo["changelog_html"].toString()); + found = true; + break; + } + } + + if (info->m_ReQueried) { + if (found) { + emit showMessage(tr("Information updated")); + } else if (result.count() == 0) { + emit showMessage(tr("No matching file found on Nexus! Maybe this file is no longer available or it was renamed?")); + } else { + SelectionDialog selection(tr("No file on Nexus matches the selected file by name. Please manually choose the correct one.")); + for (QVariant file : result) { + QVariantMap fileInfo = file.toMap(); + selection.addChoice(fileInfo["file_name"].toString(), "", file); + } + if (selection.exec() == QDialog::Accepted) { + QVariantMap fileInfo = selection.getChoiceData().toMap(); + info->m_FileInfo->name = fileInfo["name"].toString(); + info->m_FileInfo->version.parse(fileInfo["version"].toString()); + info->m_FileInfo->fileCategory = fileInfo["category_id"].toInt(); + info->m_FileInfo->fileID = fileInfo["file_id"].toInt(); + } else { + emit showMessage(tr("No matching file found on Nexus! Maybe this file is no longer available or it was renamed?")); + } + } + } else { + if (info->m_FileInfo->fileID == 0) { + qWarning("could not determine file id for %s (state %d)", + qUtf8Printable(info->m_FileName), info->m_State); + } + } + + setState(info, STATE_READY); +} + + +void DownloadManager::nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + ModRepositoryFileInfo *info = new ModRepositoryFileInfo(); + + QVariantMap result = resultData.toMap(); + info->name = result["name"].toString(); + info->version.parse(result["version"].toString()); + if (!info->version.isValid()) { + info->version = info->newestVersion; + } + info->fileName = result["file_name"].toString(); + info->fileCategory = result["category_id"].toInt(); + info->fileTime = QDateTime::fromMSecsSinceEpoch(result["uploaded_timestamp"].toLongLong()); + info->description = BBCode::convertToHTML(result["changelog_html"].toString()); + + info->repository = "Nexus"; + info->gameName = gameName; + info->modID = modID; + info->fileID = fileID; + + QObject *test = info; + m_RequestIDs.insert(m_NexusInterface->requestDownloadURL(gameName, modID, fileID, this, qVariantFromValue(test), QString())); +} + +static int evaluateFileInfoMap(const QVariantMap &map, const std::map &preferredServers) +{ + int result = 0; + + auto preference = preferredServers.find(map["name"].toString()); + + if (preference != preferredServers.end()) { + result += 100 + preference->second * 20; + } + + return result; +} + +// sort function to sort by best download server +bool DownloadManager::ServerByPreference(const std::map &preferredServers, const QVariant &LHS, const QVariant &RHS) +{ + return evaluateFileInfoMap(LHS.toMap(), preferredServers) > evaluateFileInfoMap(RHS.toMap(), preferredServers); +} + +int DownloadManager::startDownloadURLs(const QStringList &urls) +{ + ModRepositoryFileInfo info; + addDownload(urls, "", -1, -1, &info); + return m_ActiveDownloads.size() - 1; +} + +int DownloadManager::startDownloadNexusFile(int modID, int fileID) +{ + int newID = m_ActiveDownloads.size(); + addNXMDownload(QString("nxm://%1/mods/%2/files/%3").arg(m_ManagedGame->gameShortName()).arg(modID).arg(fileID)); + return newID; +} + +QString DownloadManager::downloadPath(int id) +{ + return getFilePath(id); +} + +int DownloadManager::indexByName(const QString &fileName) const +{ + for (int i = 0; i < m_ActiveDownloads.size(); ++i) { + if (m_ActiveDownloads[i]->m_FileName == fileName) { + return i; + } + } + return -1; +} + +void DownloadManager::nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + ModRepositoryFileInfo *info = qobject_cast(qvariant_cast(userData)); + QVariantList resultList = resultData.toList(); + if (resultList.length() == 0) { + removePending(gameName, modID, fileID); + emit showMessage(tr("No download server available. Please try again later.")); + return; + } + + std::sort(resultList.begin(), resultList.end(), boost::bind(&DownloadManager::ServerByPreference, m_PreferredServers, _1, _2)); + + info->userData["downloadMap"] = resultList; + + QStringList URLs; + + foreach (const QVariant &server, resultList) { + URLs.append(server.toMap()["URI"].toString()); + } + addDownload(URLs, gameName, modID, fileID, info); +} + + +void DownloadManager::nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + int index = 0; + + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++index) { + DownloadInfo *info = *iter; + if (info->m_FileInfo->modID == modID) { + if (info->m_State < STATE_FETCHINGMODINFO) { + m_ActiveDownloads.erase(iter); + delete info; + } else { + setState(info, STATE_READY); + } + emit update(index); + break; + } + } + + removePending(gameName, modID, fileID); + emit showMessage(tr("Failed to request file info from nexus: %1").arg(errorString)); +} + + +void DownloadManager::downloadFinished(int index) +{ + DownloadInfo *info; + if (index) + info = m_ActiveDownloads[index]; + else + info = findDownload(this->sender(), &index); + + if (info != nullptr) { + QNetworkReply *reply = info->m_Reply; + QByteArray data; + if (reply->isOpen() && info->m_HasData) { + data = reply->readAll(); + info->m_Output.write(data); + } + info->m_Output.close(); + TaskProgressManager::instance().forgetMe(info->m_TaskProgressId); + + bool error = false; + if ((info->m_State != STATE_CANCELING) && + (info->m_State != STATE_PAUSING)) { + bool textData = reply->header(QNetworkRequest::ContentTypeHeader).toString().startsWith("text", Qt::CaseInsensitive); + if (textData) + emit showMessage(tr("Warning: Content type is: %1").arg(reply->header(QNetworkRequest::ContentTypeHeader).toString())); + if ((info->m_Output.size() == 0) || + ((reply->error() != QNetworkReply::NoError) + && (reply->error() != QNetworkReply::OperationCanceledError))) { + if (reply->error() == QNetworkReply::UnknownContentError) + emit showMessage(tr("Download header content length: %1 downloaded file size: %2").arg(reply->header(QNetworkRequest::ContentLengthHeader).toLongLong()).arg(info->m_Output.size())); + if (info->m_Tries == 0) { + emit showMessage(tr("Download failed: %1 (%2)").arg(reply->errorString()).arg(reply->error())); + } + error = true; + setState(info, STATE_ERROR); + } + } + + if (info->m_State == STATE_CANCELING) { + setState(info, STATE_CANCELED); + } else if (info->m_State == STATE_PAUSING) { + if (info->m_Output.isOpen() && info->m_HasData) { + info->m_Output.write(info->m_Reply->readAll()); + } + setState(info, STATE_PAUSED); + } + + if (info->m_State == STATE_CANCELED || (info->m_Tries == 0 && error)) { + emit aboutToUpdate(); + info->m_Output.remove(); + delete info; + m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index); + if (error) + emit showMessage(tr("We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers.")); + emit update(-1); + } else if (info->isPausedState() || info->m_State == STATE_PAUSING) { + info->m_Output.close(); + createMetaFile(info); + emit update(index); + } else { + QString url = info->m_Urls[info->m_CurrentUrl]; + if (info->m_FileInfo->userData.contains("downloadMap")) { + foreach (const QVariant &server, info->m_FileInfo->userData["downloadMap"].toList()) { + QVariantMap serverMap = server.toMap(); + if (serverMap["URI"].toString() == url) { + int deltaTime = info->m_StartTime.secsTo(QTime::currentTime()); + if (deltaTime > 5) { + emit downloadSpeed(serverMap["Name"].toString(), (info->m_TotalSize - info->m_PreResumeSize) / deltaTime); + } // no division by zero please! Also, if the download is shorter than a few seconds, the result is way to inprecise + break; + } + } + } + + bool isNexus = info->m_FileInfo->repository == "Nexus"; + // need to change state before changing the file name, otherwise .unfinished is appended + if (isNexus) { + setState(info, STATE_FETCHINGMODINFO); + } else { + setState(info, STATE_NOFETCH); + } + + QString newName = getFileNameFromNetworkReply(reply); + QString oldName = QFileInfo(info->m_Output).fileName(); + + startDisableDirWatcher(); + if (!newName.isEmpty() && (oldName.isEmpty())) { + info->setName(getDownloadFileName(newName), true); + } else { + info->setName(m_OutputDirectory + "/" + info->m_FileName, true); // don't rename but remove the ".unfinished" extension + } + endDisableDirWatcher(); + + if (!isNexus) { + setState(info, STATE_READY); + } + + emit update(index); + } + reply->close(); + reply->deleteLater(); + + if ((info->m_Tries > 0) && error) { + --info->m_Tries; + resumeDownloadInt(index); + } + } else { + qWarning("no download index %d", index); + } +} + + +void DownloadManager::downloadError(QNetworkReply::NetworkError error) +{ + if (error != QNetworkReply::OperationCanceledError) { + QNetworkReply *reply = qobject_cast(sender()); + qWarning("%s (%d)", reply != nullptr ? qUtf8Printable(reply->errorString()) + : "Download error occured", + error); + } +} + + +void DownloadManager::metaDataChanged() +{ + int index = 0; + + DownloadInfo *info = findDownload(this->sender(), &index); + if (info != nullptr) { + QString newName = getFileNameFromNetworkReply(info->m_Reply); + if (!newName.isEmpty() && (info->m_FileName.isEmpty())) { + startDisableDirWatcher(); + info->setName(getDownloadFileName(newName), true); + endDisableDirWatcher(); + refreshAlphabeticalTranslation(); + if (!info->m_Output.isOpen() && !info->m_Output.open(QIODevice::WriteOnly | QIODevice::Append)) { + reportError(tr("failed to re-open %1").arg(info->m_FileName)); + setState(info, STATE_CANCELING); + } + } + } else { + qWarning("meta data event for unknown download"); + } +} + +void DownloadManager::directoryChanged(const QString&) +{ + if(DownloadManager::m_DirWatcherDisabler==0) + refreshList(); +} + +void DownloadManager::managedGameChanged(MOBase::IPluginGame const *managedGame) +{ + m_ManagedGame = managedGame; +} + +void DownloadManager::checkDownloadTimeout() +{ + for (int i = 0; i < m_ActiveDownloads.size(); ++i) { + if (m_ActiveDownloads[i]->m_StartTime.elapsed() - std::get<3>(m_ActiveDownloads[i]->m_SpeedDiff) > 5 * 1000 && + m_ActiveDownloads[i]->m_State == STATE_DOWNLOADING && m_ActiveDownloads[i]->m_Reply != nullptr && + m_ActiveDownloads[i]->m_Reply->isOpen()) { + pauseDownload(i); + downloadFinished(i); + resumeDownload(i); + } + } +} + +void DownloadManager::writeData(DownloadInfo *info) +{ + if (info != nullptr) { + qint64 ret = info->m_Output.write(info->m_Reply->readAll()); + if (ret < info->m_Reply->size()) { + QString fileName = info->m_FileName; // m_FileName may be destroyed after setState + setState(info, DownloadState::STATE_CANCELED); + qCritical(QString("Unable to write download \"%2\" to drive (return %1)").arg(ret).arg(info->m_FileName).toLocal8Bit()); + reportError(tr("Unable to write download to drive (return %1).\n" + "Check the drive's available storage.\n\n" + "Canceling download \"%2\"...").arg(ret).arg(fileName)); + } + } +} diff --git a/src/downloadmanager.h b/src/downloadmanager.h index 4cbe31b7..3c36143e 100644 --- a/src/downloadmanager.h +++ b/src/downloadmanager.h @@ -1,568 +1,564 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#ifndef DOWNLOADMANAGER_H -#define DOWNLOADMANAGER_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace MOBase { class IPluginGame; } - -class NexusInterface; -class PluginContainer; - -/*! - * \brief manages downloading of files and provides progress information for gui elements - **/ -class DownloadManager : public MOBase::IDownloadManager -{ - Q_OBJECT - -public: - - enum DownloadState { - STATE_STARTED = 0, - STATE_DOWNLOADING, - STATE_CANCELING, - STATE_PAUSING, - STATE_CANCELED, - STATE_PAUSED, - STATE_ERROR, - STATE_FETCHINGMODINFO, - STATE_FETCHINGFILEINFO, - STATE_NOFETCH, - STATE_READY, - STATE_INSTALLED, - STATE_UNINSTALLED - }; - -private: - - struct DownloadInfo { - ~DownloadInfo() { delete m_FileInfo; } - unsigned int m_DownloadID; - QString m_FileName; - QFile m_Output; - QNetworkReply *m_Reply; - QTime m_StartTime; - qint64 m_PreResumeSize; - std::pair m_Progress; - std::tuple m_SpeedDiff; - bool m_HasData; - DownloadState m_State; - int m_CurrentUrl; - QStringList m_Urls; - qint64 m_ResumePos; - qint64 m_TotalSize; - QDateTime m_Created; // used as a cache in DownloadManager::getFileTime, may not be valid elsewhere - - int m_Tries; - bool m_ReQueried; - - quint32 m_TaskProgressId; - - MOBase::ModRepositoryFileInfo *m_FileInfo { nullptr }; - - bool m_Hidden; - - static DownloadInfo *createNew(const MOBase::ModRepositoryFileInfo *fileInfo, const QStringList &URLs); - static DownloadInfo *createFromMeta(const QString &filePath, bool showHidden, const QString outputDirectory); - - /** - * @brief rename the file - * this will change the file name as well as the display name. It will automatically - * append .unfinished to the name if this file is still being downloaded - * @param newName the new name to setName - * @param renameFile if true, the file is assumed to exist and renamed. If the file does not - * yet exist, set this to false - **/ - void setName(QString newName, bool renameFile); - - unsigned int downloadID() { return m_DownloadID; } - - bool isPausedState(); - - QString currentURL(); - private: - static unsigned int s_NextDownloadID; - private: - DownloadInfo() : m_TotalSize(0), m_ReQueried(false), m_Hidden(false), m_SpeedDiff(std::tuple(0,0,0,0,0)), m_HasData(false) {} - }; - -public: - - /** - * @brief constructor - * - * @param nexusInterface interface to use to retrieve information from the relevant nexus page - * @param parent parent object - **/ - explicit DownloadManager(NexusInterface *nexusInterface, QObject *parent); - - ~DownloadManager(); - - /** - * @brief determine if a download is currently in progress - * - * @return true if there is currently a download in progress - **/ - bool downloadsInProgress(); - - /** - * @brief determine if a download is currently in progress, does not count paused ones. - * - * @return true if there is currently a download in progress (that is not paused already). - **/ - bool downloadsInProgressNoPause(); - - /** - * @brief set the output directory to write to - * - * @param outputDirectory the new output directory - **/ - void setOutputDirectory(const QString &outputDirectory); - - /** - * @brief disables feedback from the downlods fileSystemWhatcher untill disableDownloadsWatcherEnd() is called - * - **/ - static void startDisableDirWatcher(); - - /** - * @brief re-enables feedback from the downlods fileSystemWhatcher after disableDownloadsWatcherStart() was called - **/ - static void endDisableDirWatcher(); - - /** - * @return current download directory - **/ - QString getOutputDirectory() const { return m_OutputDirectory; } - - /** - * @brief setPreferredServers set the list of preferred servers - */ - void setPreferredServers(const std::map &preferredServers); - - /** - * @brief set the list of supported extensions - * @param extensions list of supported extensions - */ - void setSupportedExtensions(const QStringList &extensions); - - /** - * @brief sets whether hidden files are to be shown after all - */ - void setShowHidden(bool showHidden); - - void setPluginContainer(PluginContainer *pluginContainer); - - /** - * @brief download from an already open network connection - * - * @param reply the network reply to download from - * @param fileInfo information about the file, like mod id, file id, version, ... - * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again - **/ - bool addDownload(QNetworkReply *reply, const MOBase::ModRepositoryFileInfo *fileInfo); - - /** - * @brief download from an already open network connection - * - * @param reply the network reply to download from - * @param fileName the name to use for the file. This may be overridden by the name in the fileInfo-structure or if the http stream specifies a name - * @param fileInfo information previously retrieved from the nexus network - * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again - **/ - bool addDownload(QNetworkReply *reply, const QStringList &URLs, const QString &fileName, QString gameName, int modID, int fileID = 0, const MOBase::ModRepositoryFileInfo *fileInfo = new MOBase::ModRepositoryFileInfo()); - - /** - * @brief start a download using a nxm-link - * - * starts a download using a nxm-link. The download manager will first query the nexus - * page for file information. - * @param url a nxm link looking like this: nxm://skyrim/mods/1234/files/4711 - * @todo the game name encoded into the link is currently ignored, all downloads are incorrectly assumed to be for the identified game - **/ - void addNXMDownload(const QString &url); - - /** - * @brief retrieve the total number of downloads, both finished and unfinished including downloads from previous sessions - * - * @return total number of downloads - **/ - int numTotalDownloads() const; - - /** - * @brief retrieve number of pending downloads (nexus downloads for which we don't know the name and url yet) - * @return number of pending downloads - */ - int numPendingDownloads() const; - - /** - * @brief retrieve the info of a pending download - * @param index index of the pending download (index in the range [0, numPendingDownloads()[) - * @return pair of modid, fileid - */ - std::tuple getPendingDownload(int index); - - /** - * @brief retrieve the full path to the download specified by index - * - * @param index the index to look up - * @return absolute path of the file - **/ - QString getFilePath(int index) const; - - /** - * @brief retrieve a descriptive name of the download specified by index - * - * @param index index of the file to look up - * @return display name of the file - **/ - QString getDisplayName(int index) const; - - /** - * @brief retrieve the filename of the download specified by index - * - * @param index index of the file to look up - * @return name of the file - **/ - QString getFileName(int index) const; - - /** - * @brief retrieve the file size of the download specified by index - * - * @param index index of the file to look up - * @return size of the file (total size during download) - */ - qint64 getFileSize(int index) const; - - /** - * @brief retrieve the creation time of the download specified by index - * @param index index of the file to look up - * @return size of the file (total size during download) - */ - QDateTime getFileTime(int index) const; - - /** - * @brief retrieve the current progress of the download specified by index - * - * @param index index of the file to look up - * @return progress of the download in percent (integer) - **/ - std::pair getProgress(int index) const; - - /** - * @brief retrieve the current state of the download - * - * retrieve the current state of the download. A download usually goes through - * the following states: - * started -> downloading -> fetching mod info -> fetching file info -> done - * in case of downloads started via nxm-link, file information is fetched first - * - * @param index index of the file to look up - * @return the download state - **/ - DownloadState getState(int index) const; - - /** - * @param index index of the file to look up - * @return true if the nexus information for this download is not complete - **/ - bool isInfoIncomplete(int index) const; - - /** - * @brief retrieve the nexus mod id of the download specified by index - * - * @param index index of the file to look up - * @return the nexus mod id - **/ - int getModID(int index) const; - - /** - * @brief retrieve the game name of the downlaod specified by the index - * - * @param index index of the file to look up - * @return the game name - **/ - QString getGameName(int index) const; - - /** - * @brief determine if the specified file is supposed to be hidden - * @param index index of the file to look up - * @return true if the specified file is supposed to be hidden - */ - bool isHidden(int index) const; - - /** - * @brief retrieve all nexus info of the download specified by index - * - * @param index index of the file to look up - * @return the nexus mod information - **/ - const MOBase::ModRepositoryFileInfo *getFileInfo(int index) const; - - /** - * @brief mark a download as installed - * - * @param index index of the file to mark installed - */ - void markInstalled(int index); - - void markInstalled(QString download); - - /** - * @brief mark a download as uninstalled - * - * @param index index of the file to mark uninstalled - */ - void markUninstalled(int index); - - void markUninstalled(QString download); - - /** - * @brief refreshes the list of downloads - */ - void refreshList(); - - /** - * @brief Sort function for download servers - * @param LHS - * @param RHS - * @return - */ - static bool ServerByPreference(const std::map &preferredServers, const QVariant &LHS, const QVariant &RHS); - - - virtual int startDownloadURLs(const QStringList &urls); - - virtual int startDownloadNexusFile(int modID, int fileID); - - virtual QString downloadPath(int id); - - /** - * @brief retrieve a download index from the filename - * @param fileName file to look up - * @return index of that download or -1 if it wasn't found - */ - int indexByName(const QString &fileName) const; - - void pauseAll(); - -signals: - - void aboutToUpdate(); - - /** - * @brief signals that the specified download has changed - * - * @param row the row that changed. This corresponds to the download index - **/ - void update(int row); - - /** - * @brief signals the ui that a message should be displayed - * - * @param message the message to display - **/ - void showMessage(const QString &message); - - /** - * @brief emitted whenever the state of a download changes - * @param row the row that changed - * @param state the new state - */ - void stateChanged(int row, DownloadManager::DownloadState state); - - /** - * @brief emitted whenever a download completes successfully, reporting the download speed for the server used - */ - void downloadSpeed(const QString &serverName, int bytesPerSecond); - - /** - * @brief emitted whenever a new download is added to the list - */ - void downloadAdded(); - -public slots: - - /** - * @brief removes the specified download - * - * @param index index of the download to remove - * @param deleteFile if true, the file will also be deleted from disc, otherwise it is only marked as hidden. - **/ - void removeDownload(int index, bool deleteFile); - - /** - * @brief restores the specified download to view (which was previously hidden - * @param index index of the download to restore - */ - void restoreDownload(int index); - - /** - * @brief cancel the specified download. This will lead to the corresponding file to be deleted - * - * @param index index of the download to cancel - **/ - void cancelDownload(int index); - - void pauseDownload(int index); - - void resumeDownload(int index); - - void queryInfo(int index); - - void visitOnNexus(int index); - - void openFile(int index); - - void openInDownloadsFolder(int index); - - void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - - void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - - void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); - - void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); - - void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, const QString &errorString); - - void managedGameChanged(MOBase::IPluginGame const *gamePlugin); - -private slots: - - void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); - void downloadReadyRead(); - void downloadFinished(int index = 0); - void downloadError(QNetworkReply::NetworkError error); - void metaDataChanged(); - void directoryChanged(const QString &dirctory); - void checkDownloadTimeout(); - -private: - - void createMetaFile(DownloadInfo *info); - DownloadManager::DownloadInfo* getDownloadInfo(QString fileName); - -public: - - /** Get a unique filename for a download. - * - * This allows you multiple versions of download files, useful if the file - * comes from a web site with no version control - * - * @param basename: Name of the file - * - * @return Unique(ish) name - */ - QString getDownloadFileName(const QString &baseName, bool rename = false) const; - -private: - - void startDownload(QNetworkReply *reply, DownloadInfo *newDownload, bool resume); - void resumeDownloadInt(int index); - - /** - * @brief start a download from a url - * - * @param url the url to download from - * @param fileInfo information previously retrieved from the mod page - * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again - **/ - bool addDownload(const QStringList &URLs, QString gameName, int modID, int fileID, const MOBase::ModRepositoryFileInfo *fileInfo); - - // important: the caller has to lock the list-mutex, otherwise the DownloadInfo-pointer might get invalidated at any time - DownloadInfo *findDownload(QObject *reply, int *index = nullptr) const; - - void removeFile(int index, bool deleteFile); - - void refreshAlphabeticalTranslation(); - - bool ByName(int LHS, int RHS); - - QString getFileNameFromNetworkReply(QNetworkReply *reply); - - void setState(DownloadInfo *info, DownloadManager::DownloadState state); - - DownloadInfo *downloadInfoByID(unsigned int id); - - QDateTime matchDate(const QString &timeString); - - void removePending(QString gameName, int modID, int fileID); - - static QString getFileTypeString(int fileType); - - void writeData(DownloadInfo *info); - -private: - - static const int AUTOMATIC_RETRIES = 3; - -private: - - NexusInterface *m_NexusInterface; - - QVector> m_PendingDownloads; - - QVector m_ActiveDownloads; - - QString m_OutputDirectory; - std::map m_PreferredServers; - QStringList m_SupportedExtensions; - std::set m_RequestIDs; - QVector m_AlphabeticalTranslation; - - QFileSystemWatcher m_DirWatcher; - - //The dirWatcher is actually triggering off normal Mo operations such as deleting downloads or editing .meta files - //so it needs to be disabled during operations that are known to cause the creation or deletion of files in the Downloads folder. - //Notably using QSettings to edit a file creates a temporarily .lock file that causes the Watcher to trigger multiple listRefreshes freezing the ui. - static int m_DirWatcherDisabler; - - - std::map m_DownloadFails; - - bool m_ShowHidden; - - QRegExp m_DateExpression; - - MOBase::IPluginGame const *m_ManagedGame; - - QTimer m_TimeoutTimer; -}; - - - -#endif // DOWNLOADMANAGER_H +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef DOWNLOADMANAGER_H +#define DOWNLOADMANAGER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MOBase { class IPluginGame; } + +class NexusInterface; +class PluginContainer; + +/*! + * \brief manages downloading of files and provides progress information for gui elements + **/ +class DownloadManager : public MOBase::IDownloadManager +{ + Q_OBJECT + +public: + + enum DownloadState { + STATE_STARTED = 0, + STATE_DOWNLOADING, + STATE_CANCELING, + STATE_PAUSING, + STATE_CANCELED, + STATE_PAUSED, + STATE_ERROR, + STATE_FETCHINGMODINFO, + STATE_FETCHINGFILEINFO, + STATE_NOFETCH, + STATE_READY, + STATE_INSTALLED, + STATE_UNINSTALLED + }; + +private: + + struct DownloadInfo { + ~DownloadInfo() { delete m_FileInfo; } + unsigned int m_DownloadID; + QString m_FileName; + QFile m_Output; + QNetworkReply *m_Reply; + QTime m_StartTime; + qint64 m_PreResumeSize; + std::pair m_Progress; + std::tuple m_SpeedDiff; + bool m_HasData; + DownloadState m_State; + int m_CurrentUrl; + QStringList m_Urls; + qint64 m_ResumePos; + qint64 m_TotalSize; + QDateTime m_Created; // used as a cache in DownloadManager::getFileTime, may not be valid elsewhere + + int m_Tries; + bool m_ReQueried; + + quint32 m_TaskProgressId; + + MOBase::ModRepositoryFileInfo *m_FileInfo { nullptr }; + + bool m_Hidden; + + static DownloadInfo *createNew(const MOBase::ModRepositoryFileInfo *fileInfo, const QStringList &URLs); + static DownloadInfo *createFromMeta(const QString &filePath, bool showHidden, const QString outputDirectory); + + /** + * @brief rename the file + * this will change the file name as well as the display name. It will automatically + * append .unfinished to the name if this file is still being downloaded + * @param newName the new name to setName + * @param renameFile if true, the file is assumed to exist and renamed. If the file does not + * yet exist, set this to false + **/ + void setName(QString newName, bool renameFile); + + unsigned int downloadID() { return m_DownloadID; } + + bool isPausedState(); + + QString currentURL(); + private: + static unsigned int s_NextDownloadID; + private: + DownloadInfo() : m_TotalSize(0), m_ReQueried(false), m_Hidden(false), m_SpeedDiff(std::tuple(0,0,0,0,0)), m_HasData(false) {} + }; + +public: + + /** + * @brief constructor + * + * @param nexusInterface interface to use to retrieve information from the relevant nexus page + * @param parent parent object + **/ + explicit DownloadManager(NexusInterface *nexusInterface, QObject *parent); + + ~DownloadManager(); + + /** + * @brief determine if a download is currently in progress + * + * @return true if there is currently a download in progress + **/ + bool downloadsInProgress(); + + /** + * @brief determine if a download is currently in progress, does not count paused ones. + * + * @return true if there is currently a download in progress (that is not paused already). + **/ + bool downloadsInProgressNoPause(); + + /** + * @brief set the output directory to write to + * + * @param outputDirectory the new output directory + **/ + void setOutputDirectory(const QString &outputDirectory); + + /** + * @brief disables feedback from the downlods fileSystemWhatcher untill disableDownloadsWatcherEnd() is called + * + **/ + static void startDisableDirWatcher(); + + /** + * @brief re-enables feedback from the downlods fileSystemWhatcher after disableDownloadsWatcherStart() was called + **/ + static void endDisableDirWatcher(); + + /** + * @return current download directory + **/ + QString getOutputDirectory() const { return m_OutputDirectory; } + + /** + * @brief setPreferredServers set the list of preferred servers + */ + void setPreferredServers(const std::map &preferredServers); + + /** + * @brief set the list of supported extensions + * @param extensions list of supported extensions + */ + void setSupportedExtensions(const QStringList &extensions); + + /** + * @brief sets whether hidden files are to be shown after all + */ + void setShowHidden(bool showHidden); + + void setPluginContainer(PluginContainer *pluginContainer); + + /** + * @brief download from an already open network connection + * + * @param reply the network reply to download from + * @param fileInfo information about the file, like mod id, file id, version, ... + * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again + **/ + bool addDownload(QNetworkReply *reply, const MOBase::ModRepositoryFileInfo *fileInfo); + + /** + * @brief download from an already open network connection + * + * @param reply the network reply to download from + * @param fileName the name to use for the file. This may be overridden by the name in the fileInfo-structure or if the http stream specifies a name + * @param fileInfo information previously retrieved from the nexus network + * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again + **/ + bool addDownload(QNetworkReply *reply, const QStringList &URLs, const QString &fileName, QString gameName, int modID, int fileID = 0, const MOBase::ModRepositoryFileInfo *fileInfo = new MOBase::ModRepositoryFileInfo()); + + /** + * @brief start a download using a nxm-link + * + * starts a download using a nxm-link. The download manager will first query the nexus + * page for file information. + * @param url a nxm link looking like this: nxm://skyrim/mods/1234/files/4711 + * @todo the game name encoded into the link is currently ignored, all downloads are incorrectly assumed to be for the identified game + **/ + void addNXMDownload(const QString &url); + + /** + * @brief retrieve the total number of downloads, both finished and unfinished including downloads from previous sessions + * + * @return total number of downloads + **/ + int numTotalDownloads() const; + + /** + * @brief retrieve number of pending downloads (nexus downloads for which we don't know the name and url yet) + * @return number of pending downloads + */ + int numPendingDownloads() const; + + /** + * @brief retrieve the info of a pending download + * @param index index of the pending download (index in the range [0, numPendingDownloads()[) + * @return pair of modid, fileid + */ + std::tuple getPendingDownload(int index); + + /** + * @brief retrieve the full path to the download specified by index + * + * @param index the index to look up + * @return absolute path of the file + **/ + QString getFilePath(int index) const; + + /** + * @brief retrieve a descriptive name of the download specified by index + * + * @param index index of the file to look up + * @return display name of the file + **/ + QString getDisplayName(int index) const; + + /** + * @brief retrieve the filename of the download specified by index + * + * @param index index of the file to look up + * @return name of the file + **/ + QString getFileName(int index) const; + + /** + * @brief retrieve the file size of the download specified by index + * + * @param index index of the file to look up + * @return size of the file (total size during download) + */ + qint64 getFileSize(int index) const; + + /** + * @brief retrieve the creation time of the download specified by index + * @param index index of the file to look up + * @return size of the file (total size during download) + */ + QDateTime getFileTime(int index) const; + + /** + * @brief retrieve the current progress of the download specified by index + * + * @param index index of the file to look up + * @return progress of the download in percent (integer) + **/ + std::pair getProgress(int index) const; + + /** + * @brief retrieve the current state of the download + * + * retrieve the current state of the download. A download usually goes through + * the following states: + * started -> downloading -> fetching mod info -> fetching file info -> done + * in case of downloads started via nxm-link, file information is fetched first + * + * @param index index of the file to look up + * @return the download state + **/ + DownloadState getState(int index) const; + + /** + * @param index index of the file to look up + * @return true if the nexus information for this download is not complete + **/ + bool isInfoIncomplete(int index) const; + + /** + * @brief retrieve the nexus mod id of the download specified by index + * + * @param index index of the file to look up + * @return the nexus mod id + **/ + int getModID(int index) const; + + /** + * @brief retrieve the game name of the downlaod specified by the index + * + * @param index index of the file to look up + * @return the game name + **/ + QString getGameName(int index) const; + + /** + * @brief determine if the specified file is supposed to be hidden + * @param index index of the file to look up + * @return true if the specified file is supposed to be hidden + */ + bool isHidden(int index) const; + + /** + * @brief retrieve all nexus info of the download specified by index + * + * @param index index of the file to look up + * @return the nexus mod information + **/ + const MOBase::ModRepositoryFileInfo *getFileInfo(int index) const; + + /** + * @brief mark a download as installed + * + * @param index index of the file to mark installed + */ + void markInstalled(int index); + + void markInstalled(QString download); + + /** + * @brief mark a download as uninstalled + * + * @param index index of the file to mark uninstalled + */ + void markUninstalled(int index); + + void markUninstalled(QString download); + + /** + * @brief refreshes the list of downloads + */ + void refreshList(); + + /** + * @brief Sort function for download servers + * @param LHS + * @param RHS + * @return + */ + static bool ServerByPreference(const std::map &preferredServers, const QVariant &LHS, const QVariant &RHS); + + + virtual int startDownloadURLs(const QStringList &urls); + + virtual int startDownloadNexusFile(int modID, int fileID); + + virtual QString downloadPath(int id); + + /** + * @brief retrieve a download index from the filename + * @param fileName file to look up + * @return index of that download or -1 if it wasn't found + */ + int indexByName(const QString &fileName) const; + + void pauseAll(); + +signals: + + void aboutToUpdate(); + + /** + * @brief signals that the specified download has changed + * + * @param row the row that changed. This corresponds to the download index + **/ + void update(int row); + + /** + * @brief signals the ui that a message should be displayed + * + * @param message the message to display + **/ + void showMessage(const QString &message); + + /** + * @brief emitted whenever the state of a download changes + * @param row the row that changed + * @param state the new state + */ + void stateChanged(int row, DownloadManager::DownloadState state); + + /** + * @brief emitted whenever a download completes successfully, reporting the download speed for the server used + */ + void downloadSpeed(const QString &serverName, int bytesPerSecond); + + /** + * @brief emitted whenever a new download is added to the list + */ + void downloadAdded(); + +public slots: + + /** + * @brief removes the specified download + * + * @param index index of the download to remove + * @param deleteFile if true, the file will also be deleted from disc, otherwise it is only marked as hidden. + **/ + void removeDownload(int index, bool deleteFile); + + /** + * @brief restores the specified download to view (which was previously hidden + * @param index index of the download to restore + */ + void restoreDownload(int index); + + /** + * @brief cancel the specified download. This will lead to the corresponding file to be deleted + * + * @param index index of the download to cancel + **/ + void cancelDownload(int index); + + void pauseDownload(int index); + + void resumeDownload(int index); + + void queryInfo(int index); + + void visitOnNexus(int index); + + void openFile(int index); + + void openInDownloadsFolder(int index); + + void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + + void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + + void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + + void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + + void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); + + void managedGameChanged(MOBase::IPluginGame const *gamePlugin); + +private slots: + + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void downloadReadyRead(); + void downloadFinished(int index = 0); + void downloadError(QNetworkReply::NetworkError error); + void metaDataChanged(); + void directoryChanged(const QString &dirctory); + void checkDownloadTimeout(); + +private: + + void createMetaFile(DownloadInfo *info); + DownloadManager::DownloadInfo* getDownloadInfo(QString fileName); + +public: + + /** Get a unique filename for a download. + * + * This allows you multiple versions of download files, useful if the file + * comes from a web site with no version control + * + * @param basename: Name of the file + * + * @return Unique(ish) name + */ + QString getDownloadFileName(const QString &baseName, bool rename = false) const; + +private: + + void startDownload(QNetworkReply *reply, DownloadInfo *newDownload, bool resume); + void resumeDownloadInt(int index); + + /** + * @brief start a download from a url + * + * @param url the url to download from + * @param fileInfo information previously retrieved from the mod page + * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again + **/ + bool addDownload(const QStringList &URLs, QString gameName, int modID, int fileID, const MOBase::ModRepositoryFileInfo *fileInfo); + + // important: the caller has to lock the list-mutex, otherwise the DownloadInfo-pointer might get invalidated at any time + DownloadInfo *findDownload(QObject *reply, int *index = nullptr) const; + + void removeFile(int index, bool deleteFile); + + void refreshAlphabeticalTranslation(); + + bool ByName(int LHS, int RHS); + + QString getFileNameFromNetworkReply(QNetworkReply *reply); + + void setState(DownloadInfo *info, DownloadManager::DownloadState state); + + DownloadInfo *downloadInfoByID(unsigned int id); + + void removePending(QString gameName, int modID, int fileID); + + static QString getFileTypeString(int fileType); + + void writeData(DownloadInfo *info); + +private: + + static const int AUTOMATIC_RETRIES = 3; + +private: + + NexusInterface *m_NexusInterface; + + QVector> m_PendingDownloads; + + QVector m_ActiveDownloads; + + QString m_OutputDirectory; + std::map m_PreferredServers; + QStringList m_SupportedExtensions; + std::set m_RequestIDs; + QVector m_AlphabeticalTranslation; + + QFileSystemWatcher m_DirWatcher; + + //The dirWatcher is actually triggering off normal Mo operations such as deleting downloads or editing .meta files + //so it needs to be disabled during operations that are known to cause the creation or deletion of files in the Downloads folder. + //Notably using QSettings to edit a file creates a temporarily .lock file that causes the Watcher to trigger multiple listRefreshes freezing the ui. + static int m_DirWatcherDisabler; + + + std::map m_DownloadFails; + + bool m_ShowHidden; + + MOBase::IPluginGame const *m_ManagedGame; + + QTimer m_TimeoutTimer; +}; + + + +#endif // DOWNLOADMANAGER_H diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 9003d6b2..9dbb8a86 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5433,52 +5433,78 @@ void MainWindow::modDetailsUpdated(bool) } } -void MainWindow::nxmUpdatesAvailable(const std::vector &modIDs, QVariant userData, QVariant resultData, int) -{ - m_ModsToUpdate -= static_cast(modIDs.size()); - QVariantList resultList = resultData.toList(); - for (auto iter = resultList.begin(); iter != resultList.end(); ++iter) { - QVariantMap result = iter->toMap(); - // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now - IPluginGame * game = m_OrganizerCore.getGame("skyrimse"); - if (game - && result["id"].toInt() == game->nexusModOrganizerID() - && result["game_id"].toInt() == game->nexusGameID()) { - if (!result["voted_by_user"].toBool() && - Settings::instance().endorsementIntegration() && - !Settings::instance().directInterface().value("wont_endorse_MO", false).toBool()) { - ui->actionEndorseMO->setVisible(true); - } - } else { - QString gameName = m_OrganizerCore.managedGame()->gameShortName(); - bool sameNexus = false; - for (IPluginGame *game : m_PluginContainer.plugins()) { - if (game->nexusGameID() == result["game_id"].toInt()) { - gameName = game->gameShortName(); - if (game->nexusGameID() == m_OrganizerCore.managedGame()->nexusGameID()) - sameNexus = true; - break; +void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) +{ + QVariantMap resultInfo = resultData.toMap(); + QList files = resultInfo["files"].toList(); + QList fileUpdates = resultInfo["file_updates"].toList(); + bool foundUpdate = false; + m_ModsToUpdate--; + bool sameNexus = false; + for (IPluginGame *game : m_PluginContainer.plugins()) { + if (game->gameShortName() == gameName) { + if (game->nexusGameID() == m_OrganizerCore.managedGame()->nexusGameID()) + sameNexus = true; + break; + } + } + std::vector modsList = ModInfo::getByModID(gameName, modID); + // Not clear to me what this is accomplishing? + //if (sameNexus) { + // std::vector mainInfo = ModInfo::getByModID(m_OrganizerCore.managedGame()->gameShortName(), modID); + // info.reserve(info.size() + mainInfo.size()); + // info.insert(info.end(), mainInfo.begin(), mainInfo.end()); + //} + for (auto mod : modsList) { + QString installedFile = mod->getInstallationFile(); + for (auto update : fileUpdates) { + QVariantMap updateData = update.toMap(); + if (installedFile == updateData["old_file_name"].toString()) { + int currentUpdate = updateData["new_file_id"].toInt(); + bool finalUpdate = false; + while (!finalUpdate) { + finalUpdate = true; + for (auto updateScan : fileUpdates) { + QVariantMap updateScanData = updateScan.toMap(); + if (currentUpdate == updateScanData["old_file_id"].toInt()) { + currentUpdate = updateScanData["new_file_id"].toInt(); + finalUpdate = false; + break; + } + } } - } - std::vector info = ModInfo::getByModID(gameName, result["id"].toInt()); - if (sameNexus) { - std::vector mainInfo = ModInfo::getByModID(m_OrganizerCore.managedGame()->gameShortName(), result["id"].toInt()); - info.reserve(info.size() + mainInfo.size()); - info.insert(info.end(), mainInfo.begin(), mainInfo.end()); - } - for (auto iter = info.begin(); iter != info.end(); ++iter) { - (*iter)->setNewestVersion(result["version"].toString()); - (*iter)->setNexusDescription(result["description"].toString()); - if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated() && - result.contains("voted_by_user") && - Settings::instance().endorsementIntegration()) { - // don't use endorsement info if we're not logged in or if the response doesn't contain it - (*iter)->setIsEndorsed(result["voted_by_user"].toBool()); + for (auto file : files) { + QVariantMap fileData = file.toMap(); + if (fileData["file_id"].toInt() == currentUpdate) { + mod->setNewestVersion(fileData["version"].toString()); + foundUpdate = true; + } } + + break; } } + + if (foundUpdate) { + mod->updateNXMInfo(); + } + else { + NexusInterface::instance(&m_PluginContainer)->requestDescription(gameName, modID, this, QVariant(), QString()); + } } + // Old endorsement and mod info updater + //for + // if (updateData["old_file_id"].toInt() == mod->readMeta()) + // (*iter)->setNewestVersion(result["version"].toString()); + //(*iter)->setNexusDescription(result["description"].toString()); + //if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated() && + // result.contains("voted_by_user") && + // Settings::instance().endorsementIntegration()) { + // // don't use endorsement info if we're not logged in or if the response doesn't contain it + // (*iter)->setIsEndorsed(result["voted_by_user"].toBool()); + //} + if (m_ModsToUpdate <= 0) { statusBar()->hide(); m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)); @@ -5488,18 +5514,41 @@ void MainWindow::nxmUpdatesAvailable(const std::vector &modIDs, QVariant us break; } } - } else { + } + else { m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); } } +void MainWindow::nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) +{ + QVariantMap result = resultData.toMap(); + std::vector modsList = ModInfo::getByModID(gameName, modID); + for (auto mod : modsList) { + mod->setNexusDescription(result["description"].toString()); + + if ((mod->endorsedState() != ModInfo::ENDORSED_NEVER) && (result.contains("endorsement"))) { + QVariantMap endorsement = result["endorsement"].toMap(); + QString endorsementStatus = endorsement["endorse_status"].toString(); + if (endorsementStatus.compare("Endorsed") == 00) + mod->setIsEndorsed(true); + else if (endorsementStatus.compare("Abstained") == 00) + mod->setNeverEndorse(); + else + mod->setIsEndorsed(false); + } + } + disconnect(sender(), SIGNAL(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)), + this, SLOT(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int))); +} + void MainWindow::nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int) { QMap results = resultData.toMap(); - if (results["code"].toInt() == 200) { + if (results["code"].toInt() == 200 || results["code"].toInt() == 201) { if (results["status"].toString().compare("Endorsed") == 0) { QMessageBox::information(this, tr("Thank you!"), tr("Thank you for your endorsement!")); - } else { + } else if (results["status"].toString().compare("Abstained") == 0) { QMessageBox::information(this, tr("Okay."), tr("This mod will not be endorsed and will no longer ask you to endorse.")); } ui->actionEndorseMO->setVisible(false); @@ -5529,14 +5578,22 @@ void MainWindow::nxmDownloadURLs(QString, int, int, QVariant, QVariant resultDat } -void MainWindow::nxmRequestFailed(QString, int modID, int, QVariant, int, const QString &errorString) +void MainWindow::nxmRequestFailed(QString gameName, int modID, int, QVariant, int, QNetworkReply::NetworkError error, const QString &errorString) { if (modID == -1) { // must be the update-check that failed m_ModsToUpdate = 0; statusBar()->hide(); } - MessageDialog::showMessage(tr("Request to Nexus failed: %1").arg(errorString), this); + if (error == QNetworkReply::ContentAccessDenied || error == QNetworkReply::ContentNotFoundError) { + std::vector modsList = ModInfo::getByModID(gameName, modID); + for (auto mod : modsList) { + mod->setNexusID(-1); + } + MessageDialog::showMessage(tr("Mod ID %1 no longer seems to be available on Nexus.").arg(modID), this); + } else { + MessageDialog::showMessage(tr("Request to Nexus failed: %1").arg(errorString), this); + } } diff --git a/src/mainwindow.h b/src/mainwindow.h index caf94c0e..077022db 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -508,10 +508,11 @@ private slots: void modInstalled(const QString &modName); - void nxmUpdatesAvailable(const std::vector &modIDs, QVariant userData, QVariant resultData, int requestID); + void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int); void nxmDownloadURLs(QString, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); - void nxmRequestFailed(QString, int modID, int fileID, QVariant userData, int requestID, const QString &errorString); + void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); void editCategories(); void deselectFilters(); diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 905341b0..e7b7657b 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -285,30 +285,15 @@ ModInfo::ModInfo(PluginContainer *pluginContainer) } -void ModInfo::checkChunkForUpdate(PluginContainer *pluginContainer, const std::vector &modIDs, QObject *receiver, QString gameName) -{ - if (modIDs.size() != 0) { - NexusInterface::instance(pluginContainer)->requestUpdates(modIDs, receiver, QVariant(), gameName, QString()); - } -} - - int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver) { - // technically this should be 255 but those requests can take nexus fairly long, produce - // large output and may have been the cause of issue #1166 - static const int chunkSize = 64; - int result = 0; - std::vector modIDs; - - // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now - IPluginGame const *game = pluginContainer->managedGame("Skyrim Special Edition"); - if (game && game->nexusModOrganizerID()) { - modIDs.push_back(game->nexusModOrganizerID()); - checkChunkForUpdate(pluginContainer, modIDs, receiver, game->gameShortName()); - modIDs.clear(); - } + + // MO2 endorsement status is no longer available via this method - an alternative must be found + //IPluginGame const *game = pluginContainer->managedGame("Skyrim Special Edition"); + //if (game && game->nexusModOrganizerID()) { + // NexusInterface::instance(pluginContainer)->requestUpdates(game->nexusModOrganizerID(), receiver, QVariant(), game->gameShortName(), QString()); + //} std::multimap> organizedGames; for (auto mod : s_Collection) { @@ -317,24 +302,10 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv } } - QString currentGame = ""; for (auto game : organizedGames) { - if (currentGame != game.first) { - if (currentGame != "") { - checkChunkForUpdate(pluginContainer, modIDs, receiver, currentGame); - modIDs.clear(); - } - currentGame = game.first; - } - modIDs.push_back(game.second->getNexusID()); - if (modIDs.size() >= chunkSize) { - checkChunkForUpdate(pluginContainer, modIDs, receiver, currentGame); - modIDs.clear(); - } + NexusInterface::instance(pluginContainer)->requestUpdates(game.second->getNexusID(), receiver, QVariant(), game.first, QString()); } - checkChunkForUpdate(pluginContainer, modIDs, receiver, currentGame); - return result; } diff --git a/src/modinfobackup.h b/src/modinfobackup.h index da1fcd4a..f74cd111 100644 --- a/src/modinfobackup.h +++ b/src/modinfobackup.h @@ -17,6 +17,7 @@ public: virtual void setGameName(QString) {} virtual void setNexusID(int) {} virtual void endorse(bool) {} + virtual void parseNexusInfo() {} virtual int getFixedPriority() const { return -1; } virtual void ignoreUpdate(bool) {} virtual bool canBeUpdated() const { return false; } diff --git a/src/modinfodialog.cpp b/src/modinfodialog.cpp index c2ded812..d086de08 100644 --- a/src/modinfodialog.cpp +++ b/src/modinfodialog.cpp @@ -1,1577 +1,1577 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "modinfodialog.h" -#include "ui_modinfodialog.h" -#include "descriptionpage.h" -#include "mainwindow.h" - -#include "modidlineedit.h" -#include "iplugingame.h" -#include "nexusinterface.h" -#include "report.h" -#include "utility.h" -#include "messagedialog.h" -#include "bbcode.h" -#include "questionboxmemory.h" -#include "settings.h" -#include "categories.h" -#include "organizercore.h" -#include "pluginlistsortproxy.h" -#include "previewgenerator.h" -#include "previewdialog.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include - - -using namespace MOBase; -using namespace MOShared; - - -class ModFileListWidget : public QListWidgetItem { - friend bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS); -public: - ModFileListWidget(const QString &text, int sortValue, QListWidget *parent = 0) - : QListWidgetItem(text, parent, QListWidgetItem::UserType + 1), m_SortValue(sortValue) {} -private: - int m_SortValue; -}; - - -static bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS) -{ - return LHS.m_SortValue < RHS.m_SortValue; -} - - -ModInfoDialog::ModInfoDialog(ModInfo::Ptr modInfo, const DirectoryEntry *directory, bool unmanaged, OrganizerCore *organizerCore, PluginContainer *pluginContainer, QWidget *parent) - : TutorableDialog("ModInfoDialog", parent), ui(new Ui::ModInfoDialog), m_ModInfo(modInfo), - m_ThumbnailMapper(this), m_RequestStarted(false), - m_DeleteAction(nullptr), m_RenameAction(nullptr), m_OpenAction(nullptr), - m_Directory(directory), m_Origin(nullptr), - m_OrganizerCore(organizerCore), m_PluginContainer(pluginContainer) -{ - ui->setupUi(this); - this->setWindowTitle(modInfo->name()); - this->setWindowModality(Qt::WindowModal); - - m_RootPath = modInfo->absolutePath(); - - QString metaFileName = m_RootPath.mid(0).append("/meta.ini"); - m_Settings = new QSettings(metaFileName, QSettings::IniFormat); - - QLineEdit *modIDEdit = findChild("modIDEdit"); - ui->modIDEdit->setValidator(new QIntValidator(modIDEdit)); - ui->modIDEdit->setText(QString("%1").arg(modInfo->getNexusID())); - - connect(ui->modIDEdit, SIGNAL(linkClicked(QString)), this, SLOT(linkClicked(QString))); - - QString gameName = modInfo->getGameName(); - ui->sourceGameEdit->addItem(organizerCore->managedGame()->gameName(), organizerCore->managedGame()->gameShortName()); - if (organizerCore->managedGame()->validShortNames().size() == 0) { - ui->sourceGameEdit->setDisabled(true); - } else { - for (auto game : pluginContainer->plugins()) { - for (QString gameName : organizerCore->managedGame()->validShortNames()) { - if (game->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { - ui->sourceGameEdit->addItem(game->gameName(), game->gameShortName()); - break; - } - } - } - } - ui->sourceGameEdit->setCurrentIndex(ui->sourceGameEdit->findData(gameName)); - - ui->commentsEdit->setText(modInfo->comments()); - ui->notesEdit->setText(modInfo->notes()); - - ui->descriptionView->setPage(new DescriptionPage()); - - connect(&m_ThumbnailMapper, SIGNAL(mapped(const QString&)), this, SIGNAL(thumbnailClickedSignal(const QString&))); - connect(this, SIGNAL(thumbnailClickedSignal(const QString&)), this, SLOT(thumbnailClicked(const QString&))); - connect(m_ModInfo.data(), SIGNAL(modDetailsUpdated(bool)), this, SLOT(modDetailsUpdated(bool))); - connect(ui->descriptionView->page(), SIGNAL(linkClicked(QUrl)), this, SLOT(linkClicked(QUrl))); - //TODO: No easy way to delegate links - //ui->descriptionView->page()->acceptNavigationRequest(QWebEnginePage::DelegateAllLinks); - - new QShortcut(QKeySequence::Delete, this, SLOT(delete_activated())); - - if (directory->originExists(ToWString(modInfo->name()))) { - m_Origin = &directory->getOriginByName(ToWString(modInfo->name())); - if (m_Origin->isDisabled()) { - m_Origin = nullptr; - } - } - - refreshLists(); - - if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)) - { - ui->tabWidget->setTabEnabled(TAB_TEXTFILES, false); - ui->tabWidget->setTabEnabled(TAB_INIFILES, false); - ui->tabWidget->setTabEnabled(TAB_IMAGES, false); - ui->tabWidget->setTabEnabled(TAB_ESPS, false); - ui->tabWidget->setTabEnabled(TAB_CONFLICTS, false); - //ui->tabWidget->setTabEnabled(TAB_CATEGORIES, false); - addCategories(CategoryFactory::instance(), modInfo->getCategories(), ui->categoriesTree->invisibleRootItem(), 0); - refreshPrimaryCategoriesBox(); - ui->tabWidget->setTabEnabled(TAB_NEXUS, false); - //ui->tabWidget->setTabEnabled(TAB_NOTES, false); - ui->tabWidget->setTabEnabled(TAB_FILETREE, false); - } - else if (unmanaged) - { - ui->tabWidget->setTabEnabled(TAB_INIFILES, false); - ui->tabWidget->setTabEnabled(TAB_CATEGORIES, false); - ui->tabWidget->setTabEnabled(TAB_NEXUS, false); - ui->tabWidget->setTabEnabled(TAB_FILETREE, false); - ui->tabWidget->setTabEnabled(TAB_NOTES, false); - ui->tabWidget->setTabEnabled(TAB_ESPS, false); - ui->tabWidget->setTabEnabled(TAB_TEXTFILES, false); - ui->tabWidget->setTabEnabled(TAB_IMAGES, false); - } else { - initFiletree(modInfo); - addCategories(CategoryFactory::instance(), modInfo->getCategories(), ui->categoriesTree->invisibleRootItem(), 0); - refreshPrimaryCategoriesBox(); - ui->tabWidget->setTabEnabled(TAB_TEXTFILES, ui->textFileList->count() != 0); - ui->tabWidget->setTabEnabled(TAB_IMAGES, ui->thumbnailArea->count() != 0); - ui->tabWidget->setTabEnabled(TAB_ESPS, (ui->inactiveESPList->count() != 0) || (ui->activeESPList->count() != 0)); - } - initINITweaks(); - - ui->tabWidget->setTabEnabled(TAB_CONFLICTS, m_Origin != nullptr); - - - ui->endorseBtn->setVisible(Settings::instance().endorsementIntegration()); - ui->endorseBtn->setEnabled((m_ModInfo->endorsedState() == ModInfo::ENDORSED_FALSE) || - (m_ModInfo->endorsedState() == ModInfo::ENDORSED_NEVER)); - - // activate first enabled tab - for (int i = 0; i < ui->tabWidget->count(); ++i) { - if (ui->tabWidget->isTabEnabled(i)) { - ui->tabWidget->setCurrentIndex(i); - break; - } - } - - if (ui->tabWidget->currentIndex() == TAB_NEXUS) { - activateNexusTab(); - } -} - - -ModInfoDialog::~ModInfoDialog() -{ - m_ModInfo->setComments(ui->commentsEdit->text()); - //Avoid saving html stump if notes field is empty. - if (ui->notesEdit->toPlainText().isEmpty()) - m_ModInfo->setNotes(ui->notesEdit->toPlainText()); - else - m_ModInfo->setNotes(ui->notesEdit->toHtml()); - saveCategories(ui->categoriesTree->invisibleRootItem()); - saveIniTweaks(); // ini tweaks are written to the ini file directly. This is the only information not managed by ModInfo - delete ui->descriptionView->page(); - delete ui->descriptionView; - delete ui; - delete m_Settings; -} - - -void ModInfoDialog::initINITweaks() -{ - int numTweaks = m_Settings->beginReadArray("INI Tweaks"); - for (int i = 0; i < numTweaks; ++i) { - m_Settings->setArrayIndex(i); - QList items = ui->iniTweaksList->findItems(m_Settings->value("name").toString(), Qt::MatchFixedString); - if (items.size() != 0) { - items.at(0)->setCheckState(Qt::Checked); - } - } - m_Settings->endArray(); -} - -void ModInfoDialog::initFiletree(ModInfo::Ptr modInfo) -{ - ui->fileTree = findChild("fileTree"); - - m_FileSystemModel = new QFileSystemModel(this); - m_FileSystemModel->setReadOnly(false); - m_FileSystemModel->setRootPath(m_RootPath); - ui->fileTree->setModel(m_FileSystemModel); - ui->fileTree->setRootIndex(m_FileSystemModel->index(m_RootPath)); - ui->fileTree->setColumnWidth(0, 300); - - m_DeleteAction = new QAction(tr("&Delete"), ui->fileTree); - m_RenameAction = new QAction(tr("&Rename"), ui->fileTree); - m_HideAction = new QAction(tr("&Hide"), ui->fileTree); - m_UnhideAction = new QAction(tr("&Unhide"), ui->fileTree); - m_OpenAction = new QAction(tr("&Open"), ui->fileTree); - m_NewFolderAction = new QAction(tr("&New Folder"), ui->fileTree); - QObject::connect(m_DeleteAction, SIGNAL(triggered()), this, SLOT(deleteTriggered())); - QObject::connect(m_RenameAction, SIGNAL(triggered()), this, SLOT(renameTriggered())); - QObject::connect(m_OpenAction, SIGNAL(triggered()), this, SLOT(openTriggered())); - QObject::connect(m_NewFolderAction, SIGNAL(triggered()), this, SLOT(createDirectoryTriggered())); - QObject::connect(m_HideAction, SIGNAL(triggered()), this, SLOT(hideTriggered())); - connect(m_UnhideAction, SIGNAL(triggered()), this, SLOT(unhideTriggered())); -} - - -int ModInfoDialog::tabIndex(const QString &tabId) -{ - for (int i = 0; i < ui->tabWidget->count(); ++i) { - if (ui->tabWidget->widget(i)->objectName() == tabId) { - return i; - } - } - return -1; -} - - -void ModInfoDialog::restoreTabState(const QByteArray &state) -{ - QDataStream stream(state); - int count = 0; - stream >> count; - - QStringList tabIds; - - // first, only determine the new mapping - for (int newPos = 0; newPos < count; ++newPos) { - QString tabId; - stream >> tabId; - tabIds.append(tabId); - int oldPos = tabIndex(tabId); - if (oldPos != -1) { - m_RealTabPos[newPos] = oldPos; - } else { - m_RealTabPos[newPos] = newPos; - } - } - // then actually move the tabs - QTabBar *tabBar = ui->tabWidget->findChild("qt_tabwidget_tabbar"); // magic name = bad - ui->tabWidget->blockSignals(true); - for (int newPos = 0; newPos < count; ++newPos) { - QString tabId = tabIds.at(newPos); - int oldPos = tabIndex(tabId); - tabBar->moveTab(oldPos, newPos); - } - ui->tabWidget->blockSignals(false); -} - - -QByteArray ModInfoDialog::saveTabState() const -{ - QByteArray result; - QDataStream stream(&result, QIODevice::WriteOnly); - stream << ui->tabWidget->count(); - for (int i = 0; i < ui->tabWidget->count(); ++i) { - stream << ui->tabWidget->widget(i)->objectName(); - } - - return result; -} - - -void ModInfoDialog::refreshLists() -{ - int numNonConflicting = 0; - int numOverwrite = 0; - int numOverwritten = 0; - - ui->overwriteTree->clear(); - ui->overwrittenTree->clear(); - - if (m_Origin != nullptr) { - std::vector files = m_Origin->getFiles(); - for (auto iter = files.begin(); iter != files.end(); ++iter) { - QString relativeName = QDir::fromNativeSeparators(ToQString((*iter)->getRelativePath())); - QString fileName = relativeName.mid(0).prepend(m_RootPath); - bool archive; - if ((*iter)->getOrigin(archive) == m_Origin->getID()) { - std::vector>> alternatives = (*iter)->getAlternatives(); - if (!alternatives.empty()) { - std::wostringstream altString; - for (std::vector>>::iterator altIter = alternatives.begin(); - altIter != alternatives.end(); ++altIter) { - if (altIter != alternatives.begin()) { - altString << ", "; - } - altString << m_Directory->getOriginByID(altIter->first).getName(); - } - QStringList fields(relativeName.prepend("...")); - fields.append(ToQString(altString.str())); - - QTreeWidgetItem *item = new QTreeWidgetItem(fields); - item->setData(0, Qt::UserRole, fileName); - item->setData(1, Qt::UserRole, ToQString(m_Directory->getOriginByID(alternatives.back().first).getName())); - item->setData(1, Qt::UserRole + 1, alternatives.back().first); - item->setData(1, Qt::UserRole + 2, archive); - if (archive) { - QFont font = item->font(0); - font.setItalic(true); - item->setFont(0, font); - item->setFont(1, font); - } - ui->overwriteTree->addTopLevelItem(item); - ++numOverwrite; - } else {// otherwise don't display the file - ++numNonConflicting; - } - } else { - FilesOrigin &realOrigin = m_Directory->getOriginByID((*iter)->getOrigin(archive)); - QStringList fields(relativeName); - fields.append(ToQString(realOrigin.getName())); - QTreeWidgetItem *item = new QTreeWidgetItem(fields); - item->setData(0, Qt::UserRole, fileName); - item->setData(1, Qt::UserRole, ToQString(realOrigin.getName())); - item->setData(1, Qt::UserRole + 2, archive); - if (archive) { - QFont font = item->font(0); - font.setItalic(true); - item->setFont(0, font); - item->setFont(1, font); - } - ui->overwrittenTree->addTopLevelItem(item); - ++numOverwritten; - } - } - } - - if (m_RootPath.length() > 0) { - QDirIterator dirIterator(m_RootPath, QDir::Files, QDirIterator::Subdirectories); - while (dirIterator.hasNext()) { - QString fileName = dirIterator.next(); - - if (fileName.endsWith(".txt", Qt::CaseInsensitive)) { - ui->textFileList->addItem(fileName.mid(m_RootPath.length() + 1)); - } else if ((fileName.endsWith(".ini", Qt::CaseInsensitive) || fileName.endsWith(".cfg", Qt::CaseInsensitive)) && - !fileName.endsWith("meta.ini")) { - QString namePart = fileName.mid(m_RootPath.length() + 1); - if (namePart.startsWith("INI Tweaks", Qt::CaseInsensitive)) { - QListWidgetItem *newItem = new QListWidgetItem(namePart.mid(11), ui->iniTweaksList); - newItem->setData(Qt::UserRole, namePart); - newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); - newItem->setCheckState(Qt::Unchecked); - ui->iniTweaksList->addItem(newItem); - } else { - ui->iniFileList->addItem(namePart); - } - } else if (fileName.endsWith(".esp", Qt::CaseInsensitive) || - fileName.endsWith(".esm", Qt::CaseInsensitive) || - fileName.endsWith(".esl", Qt::CaseInsensitive)) { - QString relativePath = fileName.mid(m_RootPath.length() + 1); - if (relativePath.contains('/')) { - QFileInfo fileInfo(fileName); - QListWidgetItem *newItem = new QListWidgetItem(fileInfo.fileName()); - newItem->setData(Qt::UserRole, relativePath); - ui->inactiveESPList->addItem(newItem); - } else { - ui->activeESPList->addItem(relativePath); - } - } else if ((fileName.endsWith(".png", Qt::CaseInsensitive)) || - (fileName.endsWith(".jpg", Qt::CaseInsensitive))) { - QImage image = QImage(fileName); - if (!image.isNull()) { - if (static_cast(image.width()) / static_cast(image.height()) > 1.34) { - image = image.scaledToWidth(128); - } else { - image = image.scaledToHeight(96); - } - - QPushButton *thumbnailButton = new QPushButton(QPixmap::fromImage(image), ""); - thumbnailButton->setIconSize(QSize(image.width(), image.height())); - connect(thumbnailButton, SIGNAL(clicked()), &m_ThumbnailMapper, SLOT(map())); - m_ThumbnailMapper.setMapping(thumbnailButton, fileName); - ui->thumbnailArea->addWidget(thumbnailButton); - } - } - } - } - - ui->overwriteCount->display(numOverwrite); - ui->overwrittenCount->display(numOverwritten); - ui->noConflictCount->display(numNonConflicting); -} - - -void ModInfoDialog::addCategories(const CategoryFactory &factory, const std::set &enabledCategories, QTreeWidgetItem *root, int rootLevel) -{ - for (int i = 0; i < static_cast(factory.numCategories()); ++i) { - if (factory.getParentID(i) != rootLevel) { - continue; - } - int categoryID = factory.getCategoryID(i); - QTreeWidgetItem *newItem - = new QTreeWidgetItem(QStringList(factory.getCategoryName(i))); - newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); - newItem->setCheckState(0, enabledCategories.find(categoryID) - != enabledCategories.end() - ? Qt::Checked - : Qt::Unchecked); - newItem->setData(0, Qt::UserRole, categoryID); - if (factory.hasChildren(i)) { - addCategories(factory, enabledCategories, newItem, categoryID); - } - root->addChild(newItem); - } -} - - -void ModInfoDialog::saveCategories(QTreeWidgetItem *currentNode) -{ - for (int i = 0; i < currentNode->childCount(); ++i) { - QTreeWidgetItem *childNode = currentNode->child(i); - m_ModInfo->setCategory(childNode->data(0, Qt::UserRole).toInt(), childNode->checkState(0)); - saveCategories(childNode); - } -} - - -void ModInfoDialog::on_closeButton_clicked() -{ - if (allowNavigateFromTXT() && allowNavigateFromINI()) { - this->close(); - } -} - - - -QString ModInfoDialog::getModVersion() const -{ - return m_Settings->value("version", "").toString(); -} - - -const int ModInfoDialog::getModID() const -{ - return m_Settings->value("modid", 0).toInt(); -} - -void ModInfoDialog::openTab(int tab) -{ - QTabWidget *tabWidget = findChild("tabWidget"); - if (tabWidget->isTabEnabled(tab)) { - tabWidget->setCurrentIndex(tab); - } -} - -void ModInfoDialog::thumbnailClicked(const QString &fileName) -{ - QLabel *imageLabel = findChild("imageLabel"); - imageLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); - QImage image(fileName); - if (static_cast(image.width()) / static_cast(image.height()) > 1.34) { - image = image.scaledToWidth(imageLabel->geometry().width()); - } else { - image = image.scaledToHeight(imageLabel->geometry().height()); - } - imageLabel->setPixmap(QPixmap::fromImage(image)); -} - -bool ModInfoDialog::allowNavigateFromTXT() -{ - if (ui->saveTXTButton->isEnabled()) { - int res = QMessageBox::question(this, tr("Save changes?"), tr("Save changes to \"%1\"?").arg(ui->textFileView->property("currentFile").toString()), - QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); - if (res == QMessageBox::Cancel) { - return false; - } else if (res == QMessageBox::Yes) { - saveCurrentTextFile(); - } - } - return true; -} - - -bool ModInfoDialog::allowNavigateFromINI() -{ - if (ui->saveButton->isEnabled()) { - int res = QMessageBox::question(this, tr("Save changes?"), tr("Save changes to \"%1\"?").arg(ui->iniFileView->property("currentFile").toString()), - QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); - if (res == QMessageBox::Cancel) { - return false; - } else if (res == QMessageBox::Yes) { - saveCurrentIniFile(); - } - } - return true; -} - - -void ModInfoDialog::on_textFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) -{ - QString fullPath = m_RootPath + "/" + current->text(); - - QVariant currentFile = ui->textFileView->property("currentFile"); - if (currentFile.isValid() && (currentFile.toString() == fullPath)) { - // the new file is the same as the currently displayed file. May be the result of a cancelation - return; - } - - if (allowNavigateFromTXT()) { - openTextFile(fullPath); - } else { - ui->textFileList->setCurrentItem(previous, QItemSelectionModel::Current); - } -} - - -void ModInfoDialog::openTextFile(const QString &fileName) -{ - QString encoding; - ui->textFileView->setText(MOBase::readFileText(fileName, &encoding)); - ui->textFileView->setProperty("currentFile", fileName); - ui->textFileView->setProperty("encoding", encoding); - ui->saveTXTButton->setEnabled(false); -} - - -void ModInfoDialog::openIniFile(const QString &fileName) -{ - QFile iniFile(fileName); - iniFile.open(QIODevice::ReadOnly); - QByteArray buffer = iniFile.readAll(); - - QTextCodec *codec = QTextCodec::codecForUtfText(buffer, QTextCodec::codecForName("utf-8")); - QTextEdit *iniFileView = findChild("iniFileView"); - iniFileView->setText(codec->toUnicode(buffer)); - iniFileView->setProperty("currentFile", fileName); - iniFileView->setProperty("encoding", codec->name()); - iniFile.close(); - - ui->saveButton->setEnabled(false); -} - - -void ModInfoDialog::saveIniTweaks() -{ - m_Settings->remove("INI Tweaks"); - m_Settings->beginWriteArray("INI Tweaks"); - - int countEnabled = 0; - for (int i = 0; i < ui->iniTweaksList->count(); ++i) { - if (ui->iniTweaksList->item(i)->checkState() == Qt::Checked) { - m_Settings->setArrayIndex(countEnabled++); - m_Settings->setValue("name", ui->iniTweaksList->item(i)->text()); - } - } - m_Settings->endArray(); -} - - -void ModInfoDialog::on_iniFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) -{ - QString fullPath = m_RootPath + "/" + current->text(); - - QVariant currentFile = ui->iniFileView->property("currentFile"); - if (currentFile.isValid() && (currentFile.toString() == fullPath)) { - // the new file is the same as the currently displayed file. May be the result of a cancelation - return; - } - - if (allowNavigateFromINI()) { - openIniFile(fullPath); - } else { - ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current); - } -} - - -void ModInfoDialog::on_iniTweaksList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) -{ - QString fullPath = m_RootPath + "/" + current->data(Qt::UserRole).toString(); - - QVariant currentFile = ui->iniFileView->property("currentFile"); - if (currentFile.isValid() && (currentFile.toString() == fullPath)) { - // the new file is the same as the currently displayed file. May be the result of a cancelation - return; - } - - if (allowNavigateFromINI()) { - openIniFile(fullPath); - } else { - ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current); - } - -} - - -void ModInfoDialog::on_saveButton_clicked() -{ - saveCurrentIniFile(); -} - - -void ModInfoDialog::on_saveTXTButton_clicked() -{ - saveCurrentTextFile(); -} - - -void ModInfoDialog::saveCurrentTextFile() -{ - QVariant fileNameVar = ui->textFileView->property("currentFile"); - QVariant encodingVar = ui->textFileView->property("encoding"); - if (fileNameVar.isValid() && encodingVar.isValid()) { - QString fileName = fileNameVar.toString(); - QFile txtFile(fileName); - txtFile.open(QIODevice::WriteOnly); - txtFile.resize(0); - QTextCodec *codec = QTextCodec::codecForName(encodingVar.toString().toUtf8()); - QString data = ui->textFileView->toPlainText().replace("\n", "\r\n"); - txtFile.write(codec->fromUnicode(data)); - } else { - reportError("no file selected"); - } - ui->saveTXTButton->setEnabled(false); -} - - -void ModInfoDialog::saveCurrentIniFile() -{ - QVariant fileNameVar = ui->iniFileView->property("currentFile"); - QVariant encodingVar = ui->iniFileView->property("encoding"); - if (fileNameVar.isValid() && !fileNameVar.toString().isEmpty()) { - QString fileName = fileNameVar.toString(); - QDir().mkpath(QFileInfo(fileName).absolutePath()); - QFile txtFile(fileName); - txtFile.open(QIODevice::WriteOnly); - txtFile.resize(0); - QTextCodec *codec = QTextCodec::codecForName(encodingVar.toString().toUtf8()); - QString data = ui->iniFileView->toPlainText().replace("\n", "\r\n"); - txtFile.write(codec->fromUnicode(data)); - } else { - reportError("no file selected"); - } - ui->saveButton->setEnabled(false); -} - - -void ModInfoDialog::on_iniFileView_textChanged() -{ - QPushButton* saveButton = findChild("saveButton"); - saveButton->setEnabled(true); -} - - -void ModInfoDialog::on_textFileView_textChanged() -{ - ui->saveTXTButton->setEnabled(true); -} - - -void ModInfoDialog::on_activateESP_clicked() -{ - QListWidget *activeESPList = findChild("activeESPList"); - QListWidget *inactiveESPList = findChild("inactiveESPList"); - - int selectedRow = inactiveESPList->currentRow(); - if (selectedRow < 0) { - return; - } - - QListWidgetItem *selectedItem = inactiveESPList->takeItem(selectedRow); - - QDir root(m_RootPath); - bool renamed = false; - - while (root.exists(selectedItem->text())) { - bool okClicked = false; - QString newName = QInputDialog::getText(this, tr("File Exists"), tr("A file with that name exists, please enter a new one"), QLineEdit::Normal, selectedItem->text(), &okClicked); - if (!okClicked) { - inactiveESPList->insertItem(selectedRow, selectedItem); - return; - } else if (newName.size() > 0) { - selectedItem->setText(newName); - renamed = true; - } - } - - if (root.rename(selectedItem->data(Qt::UserRole).toString(), selectedItem->text())) { - activeESPList->addItem(selectedItem); - if (renamed) { - selectedItem->setData(Qt::UserRole, QVariant()); - } - } else { - inactiveESPList->insertItem(selectedRow, selectedItem); - reportError(tr("failed to move file")); - } -} - - -void ModInfoDialog::on_deactivateESP_clicked() -{ - QListWidget *activeESPList = findChild("activeESPList"); - QListWidget *inactiveESPList = findChild("inactiveESPList"); - - int selectedRow = activeESPList->currentRow(); - if (selectedRow < 0) { - return; - } - - QDir root(m_RootPath); - - QListWidgetItem *selectedItem = activeESPList->takeItem(selectedRow); - - // if we moved the file from optional to active in this session, we move the file back to - // where it came from. Otherwise, it is moved to the new folder "optional" - if (selectedItem->data(Qt::UserRole).isNull()) { - selectedItem->setData(Qt::UserRole, QString("optional/") + selectedItem->text()); - if (!root.exists("optional")) { - if (!root.mkdir("optional")) { - reportError(tr("failed to create directory \"optional\"")); - activeESPList->insertItem(selectedRow, selectedItem); - return; - } - } - } - - if (root.rename(selectedItem->text(), selectedItem->data(Qt::UserRole).toString())) { - inactiveESPList->addItem(selectedItem); - } else { - activeESPList->insertItem(selectedRow, selectedItem); - } -} - -void ModInfoDialog::on_visitNexusLabel_linkActivated(const QString &link) -{ - emit linkActivated(link); -} - -void ModInfoDialog::linkClicked(const QUrl &url) -{ - //Ideally we'd ask the mod for the game and the web service then pass the game - //and URL to the web service - if (NexusInterface::instance(m_PluginContainer)->isURLGameRelated(url)) { - - emit linkActivated(url.toString()); - } else { - ::ShellExecuteW(nullptr, L"open", ToWString(url.toString()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); - } -} - -void ModInfoDialog::linkClicked(QString url) -{ - emit linkActivated(url); -} - - -void ModInfoDialog::refreshNexusData(int modID) -{ - if ((!m_RequestStarted) && (modID > 0)) { - m_RequestStarted = true; - - m_ModInfo->updateNXMInfo(); - - MessageDialog::showMessage(tr("Info requested, please wait"), this); - } -} - - -/*void ModInfoDialog::nxmDescriptionAvailable(int, QVariant, QVariant resultData, int requestID) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - QVariantMap result = resultData.toMap(); - - if (!result["description"].isNull()) { - QString descriptionAsHTML = - QString("" - "" - "%1" - "").arg(BBCode::convertToHTML(result["description"].toString())); - -// QString descriptionAsHTML = BBCode::convertToHTML(result["description"].toString()); - ui->descriptionView->setHtml(descriptionAsHTML); - } else { - ui->descriptionView->setHtml(result["summary"].toString().append(QString("\r\n") + tr("(description incomplete, please visit nexus)"))); - } - - QLineEdit *versionEdit = findChild("versionEdit"); - QString version = result["version"].toString(); - - if (!version.isEmpty()) { - m_ModInfo->setNewestVersion(version); - - VersionInfo currentVersion(versionEdit->text()); - VersionInfo newestVersion(version); - - QPalette versionColor; - if (currentVersion < newestVersion) { - versionColor.setColor(QPalette::Text, Qt::red); - versionEdit->setToolTip(tr("Current Version: %1").arg(version)); - } else { - versionColor.setColor(QPalette::Text, Qt::green); - versionEdit->setToolTip(tr("No update available")); - } - versionEdit->setPalette(versionColor); - } -}*/ - - -QString ModInfoDialog::getFileCategory(int categoryID) -{ - switch (categoryID) { - case 1: return tr("Main"); - case 2: return tr("Update"); - case 3: return tr("Optional"); - case 4: return tr("Old"); - case 5: return tr("Misc"); - default: return tr("Unknown"); - } -} - - -void ModInfoDialog::updateVersionColor() -{ -// QPalette versionColor; - if (m_ModInfo->getVersion() != m_ModInfo->getNewestVersion()) { - ui->versionEdit->setStyleSheet("color: red"); -// versionColor.setColor(QPalette::Text, Qt::red); - ui->versionEdit->setToolTip(tr("Current Version: %1").arg(m_ModInfo->getNewestVersion().canonicalString())); - } else { - ui->versionEdit->setStyleSheet("color: green"); -// versionColor.setColor(QPalette::Text, Qt::green); - ui->versionEdit->setToolTip(tr("No update available")); - } -// ui->versionEdit->setPalette(versionColor); -} - - -void ModInfoDialog::modDetailsUpdated(bool success) -{ - if (success) { - QString nexusDescription = m_ModInfo->getNexusDescription(); - if (!nexusDescription.isEmpty()) { - /* QString input = - "[size=20]sizetest[/size]\r\n" - "[COLOR=yellow]colortest[/COLOR]\r\n" - "[center]centertest[/center]\r\n" - "[quote]quotetest 1[/quote]\r\n" - "[quote=bla]quotetest 2[/quote]\r\n" - "[url]www.skyrimnexus.com[/url]\r\n" - "[url=www.skyrimnexus.com]urltest 2[/url]\r\n" - "[ol]\r\n" - "[li]item 2[/li]" - "[*]item 1\r\n" - "[/ol]\r\n" - "[img]http://www.bbcode.org/images/bbcode_logo.png[/img]\r\n" - "[table][tr][th]headertest1[/th]" - "[th]headertest2[/th][/tr]" - "[tr][td]rowtest11[/td][td]rowtest12[/td][/tr]" - "[tr][td]rowtest21[/td][td]rowtest22[/td][/tr][/table]" - "[email=\"sherb@gmx.net\"]mail me[/email]"; - ui->descriptionView->setHtml(BBCode::convertToHTML(input));*/ - - QString descriptionAsHTML = - QString("" - "" - "%1" - "").arg(BBCode::convertToHTML(nexusDescription)); - - ui->descriptionView->page()->setHtml(descriptionAsHTML); - - // QString descriptionAsHTML = BBCode::convertToHTML(result["description"].toString()); - // ui->descriptionView->setHtml(descriptionAsHTML); - } else { - // ui->descriptionView->setHtml(result["summary"].toString().append(QString("\r\n") + tr("(description incomplete, please visit nexus)"))); - ui->descriptionView->page()->setHtml(tr("(description incomplete, please visit nexus)")); - } - - updateVersionColor(); - } -} - - -void ModInfoDialog::activateNexusTab() -{ - QLineEdit *modIDEdit = findChild("modIDEdit"); - int modID = modIDEdit->text().toInt(); - if (modID != 0) { - QString nexusLink = NexusInterface::instance(m_PluginContainer)->getModURL(modID, m_ModInfo->getGameName()); - QLabel *visitNexusLabel = findChild("visitNexusLabel"); - visitNexusLabel->setText(tr("Visit on Nexus").arg(nexusLink)); - visitNexusLabel->setToolTip(nexusLink); - - if (m_ModInfo->getNexusDescription().isEmpty() || - QDateTime::currentDateTime() > m_ModInfo->getLastNexusQuery().addDays(1)) { - refreshNexusData(modID); - } else { - this->modDetailsUpdated(true); - } - } - QLineEdit *versionEdit = findChild("versionEdit"); - QString currentVersion = m_Settings->value("version", "0.0").toString(); - versionEdit->setText(currentVersion); - ui->customUrlLineEdit->setText(m_ModInfo->getURL()); -} - - -void ModInfoDialog::on_tabWidget_currentChanged(int index) -{ - if (index == TAB_NEXUS || m_RealTabPos[index] == TAB_NEXUS) { - activateNexusTab(); - } -} - - -void ModInfoDialog::on_modIDEdit_editingFinished() -{ - int oldID = m_Settings->value("modid", 0).toInt(); - int modID = ui->modIDEdit->text().toInt(); - if (oldID != modID){ - m_ModInfo->setNexusID(modID); - - ui->descriptionView->page()->setHtml(""); - if (modID != 0) { - m_RequestStarted = false; - refreshNexusData(modID); - } - } -} - -void ModInfoDialog::on_sourceGameEdit_currentIndexChanged(int) -{ - for (auto game : m_PluginContainer->plugins()) { - if (game->gameName() == ui->sourceGameEdit->currentText()) { - m_ModInfo->setGameName(game->gameShortName()); - return; - } - } -} - -void ModInfoDialog::on_versionEdit_editingFinished() -{ - VersionInfo version(ui->versionEdit->text()); - m_ModInfo->setVersion(version); - updateVersionColor(); -} - -void ModInfoDialog::on_customUrlLineEdit_editingFinished() -{ - m_ModInfo->setURL(ui->customUrlLineEdit->text()); -} - -bool ModInfoDialog::recursiveDelete(const QModelIndex &index) -{ - for (int childRow = 0; childRow < m_FileSystemModel->rowCount(index); ++childRow) { - QModelIndex childIndex = m_FileSystemModel->index(childRow, 0, index); - if (m_FileSystemModel->isDir(childIndex)) { - if (!recursiveDelete(childIndex)) { - qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData()); - return false; - } - } else { - if (!m_FileSystemModel->remove(childIndex)) { - qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData()); - return false; - } - } - } - if (!m_FileSystemModel->remove(index)) { - qCritical("failed to delete %s", m_FileSystemModel->fileName(index).toUtf8().constData()); - return false; - } - return true; -} - - -void ModInfoDialog::on_openInExplorerButton_clicked() -{ - ::ShellExecuteW(nullptr, L"explore", ToWString(m_ModInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); -} - -void ModInfoDialog::deleteFile(const QModelIndex &index) -{ - - bool res = m_FileSystemModel->isDir(index) ? recursiveDelete(index) - : m_FileSystemModel->remove(index); - if (!res) { - QString fileName = m_FileSystemModel->fileName(index); - reportError(tr("Failed to delete %1").arg(fileName)); - } -} - -void ModInfoDialog::delete_activated() -{ - if (ui->fileTree->hasFocus()) { - QItemSelectionModel *selection = ui->fileTree->selectionModel(); - - if (selection->hasSelection() && selection->selectedRows().count() >= 1) { - - if (selection->selectedRows().count() == 0) { - return; - } - else if (selection->selectedRows().count() == 1) { - QString fileName = m_FileSystemModel->fileName(selection->selectedRows().at(0)); - if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete \"%1\"?").arg(fileName), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } - else { - if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete the selected files?"), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } - - foreach(QModelIndex index, selection->selectedRows()) { - deleteFile(index); - } - } - } -} - -void ModInfoDialog::deleteTriggered() -{ - if (m_FileSelection.count() == 0) { - return; - } else if (m_FileSelection.count() == 1) { - QString fileName = m_FileSystemModel->fileName(m_FileSelection.at(0)); - if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete \"%1\"?").arg(fileName), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } else { - if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete the selected files?"), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } - - foreach(QModelIndex index, m_FileSelection) { - deleteFile(index); - } -} - - -void ModInfoDialog::renameTriggered() -{ - QModelIndex selection = m_FileSelection.at(0); - QModelIndex index = selection.sibling(selection.row(), 0); - if (!index.isValid() || m_FileSystemModel->isReadOnly()) { - return; - } - - ui->fileTree->edit(index); -} - - -void ModInfoDialog::hideTriggered() -{ - for (QModelIndexList::const_iterator iter = m_FileSelection.constBegin(); - iter != m_FileSelection.constEnd(); ++iter) { - QString path = m_FileSystemModel->filePath(*iter); - if (!path.endsWith(ModInfo::s_HiddenExt)) { - hideFile(path); - } - } -} - - -void ModInfoDialog::unhideTriggered() -{ - for (QModelIndexList::const_iterator iter = m_FileSelection.constBegin(); - iter != m_FileSelection.constEnd(); ++iter) { - QString path = m_FileSystemModel->filePath(*iter); - if (path.endsWith(ModInfo::s_HiddenExt)) { - unhideFile(path); - } - } -} - - -void ModInfoDialog::openFile(const QModelIndex &index) -{ - QString fileName = m_FileSystemModel->filePath(index); - - HINSTANCE res = ::ShellExecuteW(nullptr, L"open", ToWString(fileName).c_str(), nullptr, nullptr, SW_SHOW); - if ((unsigned long long)res <= 32) { - qCritical("failed to invoke %s: %d", qUtf8Printable(fileName), res); - } -} - - -void ModInfoDialog::openTriggered() -{ - foreach(QModelIndex idx, m_FileSelection) { - openFile(idx); - } -} - -void ModInfoDialog::createDirectoryTriggered() -{ - QModelIndex selection = m_FileSelection.at(0); - - QModelIndex index = m_FileSystemModel->isDir(selection) ? selection - : selection.parent(); - index = index.sibling(index.row(), 0); - - QString name = tr("New Folder"); - QString path = m_FileSystemModel->filePath(index).append("/"); - - QModelIndex existingIndex = m_FileSystemModel->index(path + name); - int suffix = 1; - while (existingIndex.isValid()) { - name = tr("New Folder") + QString::number(suffix++); - existingIndex = m_FileSystemModel->index(path + name); - } - - QModelIndex newIndex = m_FileSystemModel->mkdir(index, name); - if (!newIndex.isValid()) { - reportError(tr("Failed to create \"%1\"").arg(name)); - return; - } - - ui->fileTree->setCurrentIndex(newIndex); - ui->fileTree->edit(newIndex); -} - - -void ModInfoDialog::on_fileTree_customContextMenuRequested(const QPoint &pos) -{ - QItemSelectionModel *selectionModel = ui->fileTree->selectionModel(); - m_FileSelection = selectionModel->selectedRows(0); - -// m_FileSelection = ui->fileTree->indexAt(pos); - QMenu menu(ui->fileTree); - - menu.addAction(m_NewFolderAction); - - bool hasFiles = false; - - foreach(QModelIndex idx, m_FileSelection) { - if (m_FileSystemModel->fileInfo(idx).isFile()) { - hasFiles = true; - break; - } - } - - if (selectionModel->hasSelection()) { - if (hasFiles) { - menu.addAction(m_OpenAction); - } - menu.addAction(m_RenameAction); - menu.addAction(m_DeleteAction); - if (m_FileSystemModel->fileName(m_FileSelection.at(0)).endsWith(ModInfo::s_HiddenExt)) { - menu.addAction(m_UnhideAction); - } else { - menu.addAction(m_HideAction); - } - } else { - m_FileSelection.clear(); - m_FileSelection.append(m_FileSystemModel->index(m_FileSystemModel->rootPath(), 0)); - } - menu.exec(ui->fileTree->mapToGlobal(pos)); -} - - -void ModInfoDialog::on_categoriesTree_itemChanged(QTreeWidgetItem *item, int) -{ - QTreeWidgetItem *parent = item->parent(); - while ((parent != nullptr) && ((parent->flags() & Qt::ItemIsUserCheckable) != 0) && (parent->checkState(0) == Qt::Unchecked)) { - parent->setCheckState(0, Qt::Checked); - parent = parent->parent(); - } - refreshPrimaryCategoriesBox(); -} - - -void ModInfoDialog::addCheckedCategories(QTreeWidgetItem *tree) -{ - for (int i = 0; i < tree->childCount(); ++i) { - QTreeWidgetItem *child = tree->child(i); - if (child->checkState(0) == Qt::Checked) { - ui->primaryCategoryBox->addItem(child->text(0), child->data(0, Qt::UserRole)); - addCheckedCategories(child); - } - } -} - - -void ModInfoDialog::refreshPrimaryCategoriesBox() -{ - ui->primaryCategoryBox->clear(); - int primaryCategory = m_ModInfo->getPrimaryCategory(); - addCheckedCategories(ui->categoriesTree->invisibleRootItem()); - for (int i = 0; i < ui->primaryCategoryBox->count(); ++i) { - if (ui->primaryCategoryBox->itemData(i).toInt() == primaryCategory) { - ui->primaryCategoryBox->setCurrentIndex(i); - break; - } - } -} - - -void ModInfoDialog::on_primaryCategoryBox_currentIndexChanged(int index) -{ - if (index != -1) { - m_ModInfo->setPrimaryCategory(ui->primaryCategoryBox->itemData(index).toInt()); - } -} - - -void ModInfoDialog::on_overwriteTree_itemDoubleClicked(QTreeWidgetItem *item, int) -{ - this->close(); - emit modOpen(item->data(1, Qt::UserRole).toString(), TAB_CONFLICTS); -} - - -bool ModInfoDialog::hideFile(const QString &oldName) -{ - QString newName = oldName + ModInfo::s_HiddenExt; - - if (QFileInfo(newName).exists()) { - if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a hidden version of this file. Replace it?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - if (!QFile(newName).remove()) { - QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); - return false; - } - } else { - return false; - } - } - - if (QFile::rename(oldName, newName)) { - return true; - } else { - reportError(tr("failed to rename %1 to %2").arg(oldName).arg(QDir::toNativeSeparators(newName))); - return false; - } -} - - -bool ModInfoDialog::unhideFile(const QString &oldName) -{ - QString newName = oldName.left(oldName.length() - ModInfo::s_HiddenExt.length()); - if (QFileInfo(newName).exists()) { - if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a visible version of this file. Replace it?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - if (!QFile(newName).remove()) { - QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); - return false; - } - } else { - return false; - } - } - if (QFile::rename(oldName, newName)) { - return true; - } else { - reportError(tr("failed to rename %1 to %2").arg(QDir::toNativeSeparators(oldName)).arg(QDir::toNativeSeparators(newName))); - return false; - } -} - - -void ModInfoDialog::hideConflictFile() -{ - if (hideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) { - emit originModified(m_Origin->getID()); - refreshLists(); - } -} - - -void ModInfoDialog::unhideConflictFile() -{ - if (unhideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) { - emit originModified(m_Origin->getID()); - refreshLists(); - } -} - -int ModInfoDialog::getBinaryExecuteInfo(const QFileInfo &targetInfo, QFileInfo &binaryInfo, QString &arguments) -{ - QString extension = targetInfo.suffix(); - if ((extension.compare("cmd", Qt::CaseInsensitive) == 0) || - (extension.compare("com", Qt::CaseInsensitive) == 0) || - (extension.compare("bat", Qt::CaseInsensitive) == 0)) { - binaryInfo = QFileInfo("C:\\Windows\\System32\\cmd.exe"); - arguments = QString("/C \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - return 1; - } - else if (extension.compare("exe", Qt::CaseInsensitive) == 0) { - binaryInfo = targetInfo; - return 1; - } - else if (extension.compare("jar", Qt::CaseInsensitive) == 0) { - // types that need to be injected into - std::wstring targetPathW = ToWString(targetInfo.absoluteFilePath()); - QString binaryPath; - - { // try to find java automatically - WCHAR buffer[MAX_PATH]; - if (::FindExecutableW(targetPathW.c_str(), nullptr, buffer) > (HINSTANCE)32) { - DWORD binaryType = 0UL; - if (!::GetBinaryTypeW(buffer, &binaryType)) { - qDebug("failed to determine binary type of \"%ls\": %lu", buffer, ::GetLastError()); - } - else if (binaryType == SCS_32BIT_BINARY) { - binaryPath = ToQString(buffer); - } - } - } - if (binaryPath.isEmpty() && (extension == "jar")) { - // second attempt: look to the registry - QSettings javaReg("HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment", QSettings::NativeFormat); - if (javaReg.contains("CurrentVersion")) { - QString currentVersion = javaReg.value("CurrentVersion").toString(); - binaryPath = javaReg.value(QString("%1/JavaHome").arg(currentVersion)).toString().append("\\bin\\javaw.exe"); - } - } - if (binaryPath.isEmpty()) { - binaryPath = QFileDialog::getOpenFileName(this, tr("Select binary"), QString(), tr("Binary") + " (*.exe)"); - } - if (binaryPath.isEmpty()) { - return 0; - } - binaryInfo = QFileInfo(binaryPath); - if (extension == "jar") { - arguments = QString("-jar \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - } - else { - arguments = QString("\"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - } - return 1; - } - else { - return 2; - } -} - -void ModInfoDialog::openDataFile() -{ - if (m_ConflictsContextItem != nullptr) { - QFileInfo targetInfo(m_ConflictsContextItem->data(0, Qt::UserRole).toString()); - QFileInfo binaryInfo; - QString arguments; - switch (getBinaryExecuteInfo(targetInfo, binaryInfo, arguments)) { - case 1: { - m_OrganizerCore->spawnBinaryDirect( - binaryInfo, arguments, m_OrganizerCore->currentProfile()->name(), - targetInfo.absolutePath(), "", ""); - } break; - case 2: { - ::ShellExecuteW(nullptr, L"open", - ToWString(targetInfo.absoluteFilePath()).c_str(), - nullptr, nullptr, SW_SHOWNORMAL); - } break; - default: { - // nop - } break; - } - } -} - -void ModInfoDialog::previewDataFile() -{ - QString fileName = QDir::fromNativeSeparators(m_ConflictsContextItem->data(0, Qt::UserRole).toString()); - - // what we have is an absolute path to the file in its actual location (for the primary origin) - // what we want is the path relative to the virtual data directory - - // we need to look in the virtual directory for the file to make sure the info is up to date. - - // check if the file comes from the actual data folder instead of a mod - QDir gameDirectory = m_OrganizerCore->managedGame()->dataDirectory().absolutePath(); - QString relativePath = gameDirectory.relativeFilePath(fileName); - QDir direRelativePath = gameDirectory.relativeFilePath(fileName); - // if the file is on a different drive the dirRelativePath will actually be an absolute path so we make sure that is not the case - if (!direRelativePath.isAbsolute() && !relativePath.startsWith("..")) { - fileName = relativePath; - } - else { - // crude: we search for the next slash after the base mod directory to skip everything up to the data-relative directory - int offset = m_OrganizerCore->settings().getModDirectory().size() + 1; - offset = fileName.indexOf("/", offset); - fileName = fileName.mid(offset + 1); - } - - - - const FileEntry::Ptr file = m_OrganizerCore->directoryStructure()->searchFile(ToWString(fileName), nullptr); - - if (file.get() == nullptr) { - reportError(tr("file not found: %1").arg(qUtf8Printable(fileName))); - return; - } - - // set up preview dialog - PreviewDialog preview(fileName); - auto addFunc = [&](int originId) { - FilesOrigin &origin = m_OrganizerCore->directoryStructure()->getOriginByID(originId); - QString filePath = QDir::fromNativeSeparators(ToQString(origin.getPath())) + "/" + fileName; - if (QFile::exists(filePath)) { - // it's very possible the file doesn't exist, because it's inside an archive. we don't support that - QWidget *wid = m_PluginContainer->previewGenerator().genPreview(filePath); - if (wid == nullptr) { - reportError(tr("failed to generate preview for %1").arg(filePath)); - } - else { - preview.addVariant(ToQString(origin.getName()), wid); - } - } - }; - - addFunc(file->getOrigin()); - for (auto alt : file->getAlternatives()) { - addFunc(alt.first); - } - if (preview.numVariants() > 0) { - preview.exec(); - } - else { - QMessageBox::information(this, tr("Sorry"), tr("Sorry, can't preview anything. This function currently does not support extracting from bsas.")); - } -} - - -void ModInfoDialog::on_overwriteTree_customContextMenuRequested(const QPoint &pos) -{ - m_ConflictsContextItem = ui->overwriteTree->itemAt(pos.x(), pos.y()); - - if (m_ConflictsContextItem != nullptr) { - // offer to hide/unhide file, but not for files from archives - if (!m_ConflictsContextItem->data(1, Qt::UserRole + 2).toBool()) { - QMenu menu; - if (m_ConflictsContextItem->text(0).endsWith(ModInfo::s_HiddenExt)) { - menu.addAction(tr("Un-Hide"), this, SLOT(unhideConflictFile())); - } else { - menu.addAction(tr("Hide"), this, SLOT(hideConflictFile())); - } - - menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile())); - - QString fileName = m_ConflictsContextItem->data(0, Qt::UserRole).toString(); - if (m_PluginContainer->previewGenerator().previewSupported(QFileInfo(fileName).suffix())) { - menu.addAction(tr("Preview"), this, SLOT(previewDataFile())); - } - - menu.exec(ui->overwriteTree->mapToGlobal(pos)); - } - } -} - -void ModInfoDialog::on_overwrittenTree_customContextMenuRequested(const QPoint &pos) -{ - m_ConflictsContextItem = ui->overwrittenTree->itemAt(pos.x(), pos.y()); - - if (m_ConflictsContextItem != nullptr) { - if (!m_ConflictsContextItem->data(1, Qt::UserRole + 2).toBool()) { - QMenu menu; - - menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile())); - - QString fileName = m_ConflictsContextItem->data(0, Qt::UserRole).toString(); - if (m_PluginContainer->previewGenerator().previewSupported(QFileInfo(fileName).suffix())) { - menu.addAction(tr("Preview"), this, SLOT(previewDataFile())); - } - - menu.exec(ui->overwrittenTree->mapToGlobal(pos)); - } - } -} - - -void ModInfoDialog::on_overwrittenTree_itemDoubleClicked(QTreeWidgetItem *item, int) -{ - emit modOpen(item->data(1, Qt::UserRole).toString(), TAB_CONFLICTS); - this->accept(); -} - -void ModInfoDialog::on_refreshButton_clicked() -{ - m_ModInfo->updateNXMInfo(); - - MessageDialog::showMessage(tr("Info requested, please wait"), this); -} - -void ModInfoDialog::on_endorseBtn_clicked() -{ - emit endorseMod(m_ModInfo); -} - -void ModInfoDialog::on_nextButton_clicked() -{ - int currentTab = ui->tabWidget->currentIndex(); - int tab = m_RealTabPos[currentTab]; - - emit modOpenNext(tab); - this->accept(); -} - -void ModInfoDialog::on_prevButton_clicked() -{ - int currentTab = ui->tabWidget->currentIndex(); - int tab = m_RealTabPos[currentTab]; - - emit modOpenPrev(tab); - this->accept(); -} - - -void ModInfoDialog::createTweak() -{ - QString name = QInputDialog::getText(this, tr("Name"), tr("Please enter a name")); - if (name.isNull()) { - return; - } else if (!fixDirectoryName(name)) { - QMessageBox::critical(this, tr("Error"), tr("Invalid name. Must be a valid file name")); - return; - } else if (ui->iniTweaksList->findItems(name, Qt::MatchFixedString).count() != 0) { - QMessageBox::critical(this, tr("Error"), tr("A tweak by that name exists")); - return; - } - - QListWidgetItem *newTweak = new QListWidgetItem(name + ".ini"); - newTweak->setData(Qt::UserRole, "INI Tweaks/" + name + ".ini"); - newTweak->setFlags(newTweak->flags() | Qt::ItemIsUserCheckable); - newTweak->setCheckState(Qt::Unchecked); - ui->iniTweaksList->addItem(newTweak); -} - -void ModInfoDialog::on_iniTweaksList_customContextMenuRequested(const QPoint &pos) -{ - QMenu menu; - menu.addAction(tr("Create Tweak"), this, SLOT(createTweak())); - menu.exec(ui->iniTweaksList->mapToGlobal(pos)); -} +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "modinfodialog.h" +#include "ui_modinfodialog.h" +#include "descriptionpage.h" +#include "mainwindow.h" + +#include "modidlineedit.h" +#include "iplugingame.h" +#include "nexusinterface.h" +#include "report.h" +#include "utility.h" +#include "messagedialog.h" +#include "bbcode.h" +#include "questionboxmemory.h" +#include "settings.h" +#include "categories.h" +#include "organizercore.h" +#include "pluginlistsortproxy.h" +#include "previewgenerator.h" +#include "previewdialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + + +using namespace MOBase; +using namespace MOShared; + + +class ModFileListWidget : public QListWidgetItem { + friend bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS); +public: + ModFileListWidget(const QString &text, int sortValue, QListWidget *parent = 0) + : QListWidgetItem(text, parent, QListWidgetItem::UserType + 1), m_SortValue(sortValue) {} +private: + int m_SortValue; +}; + + +static bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS) +{ + return LHS.m_SortValue < RHS.m_SortValue; +} + + +ModInfoDialog::ModInfoDialog(ModInfo::Ptr modInfo, const DirectoryEntry *directory, bool unmanaged, OrganizerCore *organizerCore, PluginContainer *pluginContainer, QWidget *parent) + : TutorableDialog("ModInfoDialog", parent), ui(new Ui::ModInfoDialog), m_ModInfo(modInfo), + m_ThumbnailMapper(this), m_RequestStarted(false), + m_DeleteAction(nullptr), m_RenameAction(nullptr), m_OpenAction(nullptr), + m_Directory(directory), m_Origin(nullptr), + m_OrganizerCore(organizerCore), m_PluginContainer(pluginContainer) +{ + ui->setupUi(this); + this->setWindowTitle(modInfo->name()); + this->setWindowModality(Qt::WindowModal); + + m_RootPath = modInfo->absolutePath(); + + QString metaFileName = m_RootPath.mid(0).append("/meta.ini"); + m_Settings = new QSettings(metaFileName, QSettings::IniFormat); + + QLineEdit *modIDEdit = findChild("modIDEdit"); + ui->modIDEdit->setValidator(new QIntValidator(modIDEdit)); + ui->modIDEdit->setText(QString("%1").arg(modInfo->getNexusID())); + + connect(ui->modIDEdit, SIGNAL(linkClicked(QString)), this, SLOT(linkClicked(QString))); + + QString gameName = modInfo->getGameName(); + ui->sourceGameEdit->addItem(organizerCore->managedGame()->gameName(), organizerCore->managedGame()->gameShortName()); + if (organizerCore->managedGame()->validShortNames().size() == 0) { + ui->sourceGameEdit->setDisabled(true); + } else { + for (auto game : pluginContainer->plugins()) { + for (QString gameName : organizerCore->managedGame()->validShortNames()) { + if (game->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { + ui->sourceGameEdit->addItem(game->gameName(), game->gameShortName()); + break; + } + } + } + } + ui->sourceGameEdit->setCurrentIndex(ui->sourceGameEdit->findData(gameName)); + + ui->commentsEdit->setText(modInfo->comments()); + ui->notesEdit->setText(modInfo->notes()); + + ui->descriptionView->setPage(new DescriptionPage()); + + connect(&m_ThumbnailMapper, SIGNAL(mapped(const QString&)), this, SIGNAL(thumbnailClickedSignal(const QString&))); + connect(this, SIGNAL(thumbnailClickedSignal(const QString&)), this, SLOT(thumbnailClicked(const QString&))); + connect(m_ModInfo.data(), SIGNAL(modDetailsUpdated(bool)), this, SLOT(modDetailsUpdated(bool))); + connect(ui->descriptionView->page(), SIGNAL(linkClicked(QUrl)), this, SLOT(linkClicked(QUrl))); + //TODO: No easy way to delegate links + //ui->descriptionView->page()->acceptNavigationRequest(QWebEnginePage::DelegateAllLinks); + + new QShortcut(QKeySequence::Delete, this, SLOT(delete_activated())); + + if (directory->originExists(ToWString(modInfo->name()))) { + m_Origin = &directory->getOriginByName(ToWString(modInfo->name())); + if (m_Origin->isDisabled()) { + m_Origin = nullptr; + } + } + + refreshLists(); + + if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)) + { + ui->tabWidget->setTabEnabled(TAB_TEXTFILES, false); + ui->tabWidget->setTabEnabled(TAB_INIFILES, false); + ui->tabWidget->setTabEnabled(TAB_IMAGES, false); + ui->tabWidget->setTabEnabled(TAB_ESPS, false); + ui->tabWidget->setTabEnabled(TAB_CONFLICTS, false); + //ui->tabWidget->setTabEnabled(TAB_CATEGORIES, false); + addCategories(CategoryFactory::instance(), modInfo->getCategories(), ui->categoriesTree->invisibleRootItem(), 0); + refreshPrimaryCategoriesBox(); + ui->tabWidget->setTabEnabled(TAB_NEXUS, false); + //ui->tabWidget->setTabEnabled(TAB_NOTES, false); + ui->tabWidget->setTabEnabled(TAB_FILETREE, false); + } + else if (unmanaged) + { + ui->tabWidget->setTabEnabled(TAB_INIFILES, false); + ui->tabWidget->setTabEnabled(TAB_CATEGORIES, false); + ui->tabWidget->setTabEnabled(TAB_NEXUS, false); + ui->tabWidget->setTabEnabled(TAB_FILETREE, false); + ui->tabWidget->setTabEnabled(TAB_NOTES, false); + ui->tabWidget->setTabEnabled(TAB_ESPS, false); + ui->tabWidget->setTabEnabled(TAB_TEXTFILES, false); + ui->tabWidget->setTabEnabled(TAB_IMAGES, false); + } else { + initFiletree(modInfo); + addCategories(CategoryFactory::instance(), modInfo->getCategories(), ui->categoriesTree->invisibleRootItem(), 0); + refreshPrimaryCategoriesBox(); + ui->tabWidget->setTabEnabled(TAB_TEXTFILES, ui->textFileList->count() != 0); + ui->tabWidget->setTabEnabled(TAB_IMAGES, ui->thumbnailArea->count() != 0); + ui->tabWidget->setTabEnabled(TAB_ESPS, (ui->inactiveESPList->count() != 0) || (ui->activeESPList->count() != 0)); + } + initINITweaks(); + + ui->tabWidget->setTabEnabled(TAB_CONFLICTS, m_Origin != nullptr); + + + ui->endorseBtn->setVisible(Settings::instance().endorsementIntegration()); + ui->endorseBtn->setEnabled((m_ModInfo->endorsedState() == ModInfo::ENDORSED_FALSE) || + (m_ModInfo->endorsedState() == ModInfo::ENDORSED_NEVER)); + + // activate first enabled tab + for (int i = 0; i < ui->tabWidget->count(); ++i) { + if (ui->tabWidget->isTabEnabled(i)) { + ui->tabWidget->setCurrentIndex(i); + break; + } + } + + if (ui->tabWidget->currentIndex() == TAB_NEXUS) { + activateNexusTab(); + } +} + + +ModInfoDialog::~ModInfoDialog() +{ + m_ModInfo->setComments(ui->commentsEdit->text()); + //Avoid saving html stump if notes field is empty. + if (ui->notesEdit->toPlainText().isEmpty()) + m_ModInfo->setNotes(ui->notesEdit->toPlainText()); + else + m_ModInfo->setNotes(ui->notesEdit->toHtml()); + saveCategories(ui->categoriesTree->invisibleRootItem()); + saveIniTweaks(); // ini tweaks are written to the ini file directly. This is the only information not managed by ModInfo + delete ui->descriptionView->page(); + delete ui->descriptionView; + delete ui; + delete m_Settings; +} + + +void ModInfoDialog::initINITweaks() +{ + int numTweaks = m_Settings->beginReadArray("INI Tweaks"); + for (int i = 0; i < numTweaks; ++i) { + m_Settings->setArrayIndex(i); + QList items = ui->iniTweaksList->findItems(m_Settings->value("name").toString(), Qt::MatchFixedString); + if (items.size() != 0) { + items.at(0)->setCheckState(Qt::Checked); + } + } + m_Settings->endArray(); +} + +void ModInfoDialog::initFiletree(ModInfo::Ptr modInfo) +{ + ui->fileTree = findChild("fileTree"); + + m_FileSystemModel = new QFileSystemModel(this); + m_FileSystemModel->setReadOnly(false); + m_FileSystemModel->setRootPath(m_RootPath); + ui->fileTree->setModel(m_FileSystemModel); + ui->fileTree->setRootIndex(m_FileSystemModel->index(m_RootPath)); + ui->fileTree->setColumnWidth(0, 300); + + m_DeleteAction = new QAction(tr("&Delete"), ui->fileTree); + m_RenameAction = new QAction(tr("&Rename"), ui->fileTree); + m_HideAction = new QAction(tr("&Hide"), ui->fileTree); + m_UnhideAction = new QAction(tr("&Unhide"), ui->fileTree); + m_OpenAction = new QAction(tr("&Open"), ui->fileTree); + m_NewFolderAction = new QAction(tr("&New Folder"), ui->fileTree); + QObject::connect(m_DeleteAction, SIGNAL(triggered()), this, SLOT(deleteTriggered())); + QObject::connect(m_RenameAction, SIGNAL(triggered()), this, SLOT(renameTriggered())); + QObject::connect(m_OpenAction, SIGNAL(triggered()), this, SLOT(openTriggered())); + QObject::connect(m_NewFolderAction, SIGNAL(triggered()), this, SLOT(createDirectoryTriggered())); + QObject::connect(m_HideAction, SIGNAL(triggered()), this, SLOT(hideTriggered())); + connect(m_UnhideAction, SIGNAL(triggered()), this, SLOT(unhideTriggered())); +} + + +int ModInfoDialog::tabIndex(const QString &tabId) +{ + for (int i = 0; i < ui->tabWidget->count(); ++i) { + if (ui->tabWidget->widget(i)->objectName() == tabId) { + return i; + } + } + return -1; +} + + +void ModInfoDialog::restoreTabState(const QByteArray &state) +{ + QDataStream stream(state); + int count = 0; + stream >> count; + + QStringList tabIds; + + // first, only determine the new mapping + for (int newPos = 0; newPos < count; ++newPos) { + QString tabId; + stream >> tabId; + tabIds.append(tabId); + int oldPos = tabIndex(tabId); + if (oldPos != -1) { + m_RealTabPos[newPos] = oldPos; + } else { + m_RealTabPos[newPos] = newPos; + } + } + // then actually move the tabs + QTabBar *tabBar = ui->tabWidget->findChild("qt_tabwidget_tabbar"); // magic name = bad + ui->tabWidget->blockSignals(true); + for (int newPos = 0; newPos < count; ++newPos) { + QString tabId = tabIds.at(newPos); + int oldPos = tabIndex(tabId); + tabBar->moveTab(oldPos, newPos); + } + ui->tabWidget->blockSignals(false); +} + + +QByteArray ModInfoDialog::saveTabState() const +{ + QByteArray result; + QDataStream stream(&result, QIODevice::WriteOnly); + stream << ui->tabWidget->count(); + for (int i = 0; i < ui->tabWidget->count(); ++i) { + stream << ui->tabWidget->widget(i)->objectName(); + } + + return result; +} + + +void ModInfoDialog::refreshLists() +{ + int numNonConflicting = 0; + int numOverwrite = 0; + int numOverwritten = 0; + + ui->overwriteTree->clear(); + ui->overwrittenTree->clear(); + + if (m_Origin != nullptr) { + std::vector files = m_Origin->getFiles(); + for (auto iter = files.begin(); iter != files.end(); ++iter) { + QString relativeName = QDir::fromNativeSeparators(ToQString((*iter)->getRelativePath())); + QString fileName = relativeName.mid(0).prepend(m_RootPath); + bool archive; + if ((*iter)->getOrigin(archive) == m_Origin->getID()) { + std::vector>> alternatives = (*iter)->getAlternatives(); + if (!alternatives.empty()) { + std::wostringstream altString; + for (std::vector>>::iterator altIter = alternatives.begin(); + altIter != alternatives.end(); ++altIter) { + if (altIter != alternatives.begin()) { + altString << ", "; + } + altString << m_Directory->getOriginByID(altIter->first).getName(); + } + QStringList fields(relativeName.prepend("...")); + fields.append(ToQString(altString.str())); + + QTreeWidgetItem *item = new QTreeWidgetItem(fields); + item->setData(0, Qt::UserRole, fileName); + item->setData(1, Qt::UserRole, ToQString(m_Directory->getOriginByID(alternatives.back().first).getName())); + item->setData(1, Qt::UserRole + 1, alternatives.back().first); + item->setData(1, Qt::UserRole + 2, archive); + if (archive) { + QFont font = item->font(0); + font.setItalic(true); + item->setFont(0, font); + item->setFont(1, font); + } + ui->overwriteTree->addTopLevelItem(item); + ++numOverwrite; + } else {// otherwise don't display the file + ++numNonConflicting; + } + } else { + FilesOrigin &realOrigin = m_Directory->getOriginByID((*iter)->getOrigin(archive)); + QStringList fields(relativeName); + fields.append(ToQString(realOrigin.getName())); + QTreeWidgetItem *item = new QTreeWidgetItem(fields); + item->setData(0, Qt::UserRole, fileName); + item->setData(1, Qt::UserRole, ToQString(realOrigin.getName())); + item->setData(1, Qt::UserRole + 2, archive); + if (archive) { + QFont font = item->font(0); + font.setItalic(true); + item->setFont(0, font); + item->setFont(1, font); + } + ui->overwrittenTree->addTopLevelItem(item); + ++numOverwritten; + } + } + } + + if (m_RootPath.length() > 0) { + QDirIterator dirIterator(m_RootPath, QDir::Files, QDirIterator::Subdirectories); + while (dirIterator.hasNext()) { + QString fileName = dirIterator.next(); + + if (fileName.endsWith(".txt", Qt::CaseInsensitive)) { + ui->textFileList->addItem(fileName.mid(m_RootPath.length() + 1)); + } else if ((fileName.endsWith(".ini", Qt::CaseInsensitive) || fileName.endsWith(".cfg", Qt::CaseInsensitive)) && + !fileName.endsWith("meta.ini")) { + QString namePart = fileName.mid(m_RootPath.length() + 1); + if (namePart.startsWith("INI Tweaks", Qt::CaseInsensitive)) { + QListWidgetItem *newItem = new QListWidgetItem(namePart.mid(11), ui->iniTweaksList); + newItem->setData(Qt::UserRole, namePart); + newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); + newItem->setCheckState(Qt::Unchecked); + ui->iniTweaksList->addItem(newItem); + } else { + ui->iniFileList->addItem(namePart); + } + } else if (fileName.endsWith(".esp", Qt::CaseInsensitive) || + fileName.endsWith(".esm", Qt::CaseInsensitive) || + fileName.endsWith(".esl", Qt::CaseInsensitive)) { + QString relativePath = fileName.mid(m_RootPath.length() + 1); + if (relativePath.contains('/')) { + QFileInfo fileInfo(fileName); + QListWidgetItem *newItem = new QListWidgetItem(fileInfo.fileName()); + newItem->setData(Qt::UserRole, relativePath); + ui->inactiveESPList->addItem(newItem); + } else { + ui->activeESPList->addItem(relativePath); + } + } else if ((fileName.endsWith(".png", Qt::CaseInsensitive)) || + (fileName.endsWith(".jpg", Qt::CaseInsensitive))) { + QImage image = QImage(fileName); + if (!image.isNull()) { + if (static_cast(image.width()) / static_cast(image.height()) > 1.34) { + image = image.scaledToWidth(128); + } else { + image = image.scaledToHeight(96); + } + + QPushButton *thumbnailButton = new QPushButton(QPixmap::fromImage(image), ""); + thumbnailButton->setIconSize(QSize(image.width(), image.height())); + connect(thumbnailButton, SIGNAL(clicked()), &m_ThumbnailMapper, SLOT(map())); + m_ThumbnailMapper.setMapping(thumbnailButton, fileName); + ui->thumbnailArea->addWidget(thumbnailButton); + } + } + } + } + + ui->overwriteCount->display(numOverwrite); + ui->overwrittenCount->display(numOverwritten); + ui->noConflictCount->display(numNonConflicting); +} + + +void ModInfoDialog::addCategories(const CategoryFactory &factory, const std::set &enabledCategories, QTreeWidgetItem *root, int rootLevel) +{ + for (int i = 0; i < static_cast(factory.numCategories()); ++i) { + if (factory.getParentID(i) != rootLevel) { + continue; + } + int categoryID = factory.getCategoryID(i); + QTreeWidgetItem *newItem + = new QTreeWidgetItem(QStringList(factory.getCategoryName(i))); + newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); + newItem->setCheckState(0, enabledCategories.find(categoryID) + != enabledCategories.end() + ? Qt::Checked + : Qt::Unchecked); + newItem->setData(0, Qt::UserRole, categoryID); + if (factory.hasChildren(i)) { + addCategories(factory, enabledCategories, newItem, categoryID); + } + root->addChild(newItem); + } +} + + +void ModInfoDialog::saveCategories(QTreeWidgetItem *currentNode) +{ + for (int i = 0; i < currentNode->childCount(); ++i) { + QTreeWidgetItem *childNode = currentNode->child(i); + m_ModInfo->setCategory(childNode->data(0, Qt::UserRole).toInt(), childNode->checkState(0)); + saveCategories(childNode); + } +} + + +void ModInfoDialog::on_closeButton_clicked() +{ + if (allowNavigateFromTXT() && allowNavigateFromINI()) { + this->close(); + } +} + + + +QString ModInfoDialog::getModVersion() const +{ + return m_Settings->value("version", "").toString(); +} + + +const int ModInfoDialog::getModID() const +{ + return m_Settings->value("modid", 0).toInt(); +} + +void ModInfoDialog::openTab(int tab) +{ + QTabWidget *tabWidget = findChild("tabWidget"); + if (tabWidget->isTabEnabled(tab)) { + tabWidget->setCurrentIndex(tab); + } +} + +void ModInfoDialog::thumbnailClicked(const QString &fileName) +{ + QLabel *imageLabel = findChild("imageLabel"); + imageLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + QImage image(fileName); + if (static_cast(image.width()) / static_cast(image.height()) > 1.34) { + image = image.scaledToWidth(imageLabel->geometry().width()); + } else { + image = image.scaledToHeight(imageLabel->geometry().height()); + } + imageLabel->setPixmap(QPixmap::fromImage(image)); +} + +bool ModInfoDialog::allowNavigateFromTXT() +{ + if (ui->saveTXTButton->isEnabled()) { + int res = QMessageBox::question(this, tr("Save changes?"), tr("Save changes to \"%1\"?").arg(ui->textFileView->property("currentFile").toString()), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + if (res == QMessageBox::Cancel) { + return false; + } else if (res == QMessageBox::Yes) { + saveCurrentTextFile(); + } + } + return true; +} + + +bool ModInfoDialog::allowNavigateFromINI() +{ + if (ui->saveButton->isEnabled()) { + int res = QMessageBox::question(this, tr("Save changes?"), tr("Save changes to \"%1\"?").arg(ui->iniFileView->property("currentFile").toString()), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + if (res == QMessageBox::Cancel) { + return false; + } else if (res == QMessageBox::Yes) { + saveCurrentIniFile(); + } + } + return true; +} + + +void ModInfoDialog::on_textFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) +{ + QString fullPath = m_RootPath + "/" + current->text(); + + QVariant currentFile = ui->textFileView->property("currentFile"); + if (currentFile.isValid() && (currentFile.toString() == fullPath)) { + // the new file is the same as the currently displayed file. May be the result of a cancelation + return; + } + + if (allowNavigateFromTXT()) { + openTextFile(fullPath); + } else { + ui->textFileList->setCurrentItem(previous, QItemSelectionModel::Current); + } +} + + +void ModInfoDialog::openTextFile(const QString &fileName) +{ + QString encoding; + ui->textFileView->setText(MOBase::readFileText(fileName, &encoding)); + ui->textFileView->setProperty("currentFile", fileName); + ui->textFileView->setProperty("encoding", encoding); + ui->saveTXTButton->setEnabled(false); +} + + +void ModInfoDialog::openIniFile(const QString &fileName) +{ + QFile iniFile(fileName); + iniFile.open(QIODevice::ReadOnly); + QByteArray buffer = iniFile.readAll(); + + QTextCodec *codec = QTextCodec::codecForUtfText(buffer, QTextCodec::codecForName("utf-8")); + QTextEdit *iniFileView = findChild("iniFileView"); + iniFileView->setText(codec->toUnicode(buffer)); + iniFileView->setProperty("currentFile", fileName); + iniFileView->setProperty("encoding", codec->name()); + iniFile.close(); + + ui->saveButton->setEnabled(false); +} + + +void ModInfoDialog::saveIniTweaks() +{ + m_Settings->remove("INI Tweaks"); + m_Settings->beginWriteArray("INI Tweaks"); + + int countEnabled = 0; + for (int i = 0; i < ui->iniTweaksList->count(); ++i) { + if (ui->iniTweaksList->item(i)->checkState() == Qt::Checked) { + m_Settings->setArrayIndex(countEnabled++); + m_Settings->setValue("name", ui->iniTweaksList->item(i)->text()); + } + } + m_Settings->endArray(); +} + + +void ModInfoDialog::on_iniFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) +{ + QString fullPath = m_RootPath + "/" + current->text(); + + QVariant currentFile = ui->iniFileView->property("currentFile"); + if (currentFile.isValid() && (currentFile.toString() == fullPath)) { + // the new file is the same as the currently displayed file. May be the result of a cancelation + return; + } + + if (allowNavigateFromINI()) { + openIniFile(fullPath); + } else { + ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current); + } +} + + +void ModInfoDialog::on_iniTweaksList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) +{ + QString fullPath = m_RootPath + "/" + current->data(Qt::UserRole).toString(); + + QVariant currentFile = ui->iniFileView->property("currentFile"); + if (currentFile.isValid() && (currentFile.toString() == fullPath)) { + // the new file is the same as the currently displayed file. May be the result of a cancelation + return; + } + + if (allowNavigateFromINI()) { + openIniFile(fullPath); + } else { + ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current); + } + +} + + +void ModInfoDialog::on_saveButton_clicked() +{ + saveCurrentIniFile(); +} + + +void ModInfoDialog::on_saveTXTButton_clicked() +{ + saveCurrentTextFile(); +} + + +void ModInfoDialog::saveCurrentTextFile() +{ + QVariant fileNameVar = ui->textFileView->property("currentFile"); + QVariant encodingVar = ui->textFileView->property("encoding"); + if (fileNameVar.isValid() && encodingVar.isValid()) { + QString fileName = fileNameVar.toString(); + QFile txtFile(fileName); + txtFile.open(QIODevice::WriteOnly); + txtFile.resize(0); + QTextCodec *codec = QTextCodec::codecForName(encodingVar.toString().toUtf8()); + QString data = ui->textFileView->toPlainText().replace("\n", "\r\n"); + txtFile.write(codec->fromUnicode(data)); + } else { + reportError("no file selected"); + } + ui->saveTXTButton->setEnabled(false); +} + + +void ModInfoDialog::saveCurrentIniFile() +{ + QVariant fileNameVar = ui->iniFileView->property("currentFile"); + QVariant encodingVar = ui->iniFileView->property("encoding"); + if (fileNameVar.isValid() && !fileNameVar.toString().isEmpty()) { + QString fileName = fileNameVar.toString(); + QDir().mkpath(QFileInfo(fileName).absolutePath()); + QFile txtFile(fileName); + txtFile.open(QIODevice::WriteOnly); + txtFile.resize(0); + QTextCodec *codec = QTextCodec::codecForName(encodingVar.toString().toUtf8()); + QString data = ui->iniFileView->toPlainText().replace("\n", "\r\n"); + txtFile.write(codec->fromUnicode(data)); + } else { + reportError("no file selected"); + } + ui->saveButton->setEnabled(false); +} + + +void ModInfoDialog::on_iniFileView_textChanged() +{ + QPushButton* saveButton = findChild("saveButton"); + saveButton->setEnabled(true); +} + + +void ModInfoDialog::on_textFileView_textChanged() +{ + ui->saveTXTButton->setEnabled(true); +} + + +void ModInfoDialog::on_activateESP_clicked() +{ + QListWidget *activeESPList = findChild("activeESPList"); + QListWidget *inactiveESPList = findChild("inactiveESPList"); + + int selectedRow = inactiveESPList->currentRow(); + if (selectedRow < 0) { + return; + } + + QListWidgetItem *selectedItem = inactiveESPList->takeItem(selectedRow); + + QDir root(m_RootPath); + bool renamed = false; + + while (root.exists(selectedItem->text())) { + bool okClicked = false; + QString newName = QInputDialog::getText(this, tr("File Exists"), tr("A file with that name exists, please enter a new one"), QLineEdit::Normal, selectedItem->text(), &okClicked); + if (!okClicked) { + inactiveESPList->insertItem(selectedRow, selectedItem); + return; + } else if (newName.size() > 0) { + selectedItem->setText(newName); + renamed = true; + } + } + + if (root.rename(selectedItem->data(Qt::UserRole).toString(), selectedItem->text())) { + activeESPList->addItem(selectedItem); + if (renamed) { + selectedItem->setData(Qt::UserRole, QVariant()); + } + } else { + inactiveESPList->insertItem(selectedRow, selectedItem); + reportError(tr("failed to move file")); + } +} + + +void ModInfoDialog::on_deactivateESP_clicked() +{ + QListWidget *activeESPList = findChild("activeESPList"); + QListWidget *inactiveESPList = findChild("inactiveESPList"); + + int selectedRow = activeESPList->currentRow(); + if (selectedRow < 0) { + return; + } + + QDir root(m_RootPath); + + QListWidgetItem *selectedItem = activeESPList->takeItem(selectedRow); + + // if we moved the file from optional to active in this session, we move the file back to + // where it came from. Otherwise, it is moved to the new folder "optional" + if (selectedItem->data(Qt::UserRole).isNull()) { + selectedItem->setData(Qt::UserRole, QString("optional/") + selectedItem->text()); + if (!root.exists("optional")) { + if (!root.mkdir("optional")) { + reportError(tr("failed to create directory \"optional\"")); + activeESPList->insertItem(selectedRow, selectedItem); + return; + } + } + } + + if (root.rename(selectedItem->text(), selectedItem->data(Qt::UserRole).toString())) { + inactiveESPList->addItem(selectedItem); + } else { + activeESPList->insertItem(selectedRow, selectedItem); + } +} + +void ModInfoDialog::on_visitNexusLabel_linkActivated(const QString &link) +{ + emit linkActivated(link); +} + +void ModInfoDialog::linkClicked(const QUrl &url) +{ + //Ideally we'd ask the mod for the game and the web service then pass the game + //and URL to the web service + if (NexusInterface::instance(m_PluginContainer)->isURLGameRelated(url)) { + + emit linkActivated(url.toString()); + } else { + ::ShellExecuteW(nullptr, L"open", ToWString(url.toString()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } +} + +void ModInfoDialog::linkClicked(QString url) +{ + emit linkActivated(url); +} + + +void ModInfoDialog::refreshNexusData(int modID) +{ + if ((!m_RequestStarted) && (modID > 0)) { + m_RequestStarted = true; + + m_ModInfo->updateNXMInfo(); + + MessageDialog::showMessage(tr("Info requested, please wait"), this); + } +} + + +/*void ModInfoDialog::nxmDescriptionAvailable(int, QVariant, QVariant resultData, int requestID) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + QVariantMap result = resultData.toMap(); + + if (!result["description"].isNull()) { + QString descriptionAsHTML = + QString("" + "" + "%1" + "").arg(BBCode::convertToHTML(result["description"].toString())); + +// QString descriptionAsHTML = BBCode::convertToHTML(result["description"].toString()); + ui->descriptionView->setHtml(descriptionAsHTML); + } else { + ui->descriptionView->setHtml(result["summary"].toString().append(QString("\r\n") + tr("(description incomplete, please visit nexus)"))); + } + + QLineEdit *versionEdit = findChild("versionEdit"); + QString version = result["version"].toString(); + + if (!version.isEmpty()) { + m_ModInfo->setNewestVersion(version); + + VersionInfo currentVersion(versionEdit->text()); + VersionInfo newestVersion(version); + + QPalette versionColor; + if (currentVersion < newestVersion) { + versionColor.setColor(QPalette::Text, Qt::red); + versionEdit->setToolTip(tr("Current Version: %1").arg(version)); + } else { + versionColor.setColor(QPalette::Text, Qt::green); + versionEdit->setToolTip(tr("No update available")); + } + versionEdit->setPalette(versionColor); + } +}*/ + + +QString ModInfoDialog::getFileCategory(int categoryID) +{ + switch (categoryID) { + case 1: return tr("Main"); + case 2: return tr("Update"); + case 3: return tr("Optional"); + case 4: return tr("Old"); + case 6: return tr("Deleted"); + default: return tr("Unknown"); + } +} + + +void ModInfoDialog::updateVersionColor() +{ +// QPalette versionColor; + if (m_ModInfo->getVersion() != m_ModInfo->getNewestVersion()) { + ui->versionEdit->setStyleSheet("color: red"); +// versionColor.setColor(QPalette::Text, Qt::red); + ui->versionEdit->setToolTip(tr("Current Version: %1").arg(m_ModInfo->getNewestVersion().canonicalString())); + } else { + ui->versionEdit->setStyleSheet("color: green"); +// versionColor.setColor(QPalette::Text, Qt::green); + ui->versionEdit->setToolTip(tr("No update available")); + } +// ui->versionEdit->setPalette(versionColor); +} + + +void ModInfoDialog::modDetailsUpdated(bool success) +{ + if (success) { + QString nexusDescription = m_ModInfo->getNexusDescription(); + if (!nexusDescription.isEmpty()) { + /* QString input = + "[size=20]sizetest[/size]\r\n" + "[COLOR=yellow]colortest[/COLOR]\r\n" + "[center]centertest[/center]\r\n" + "[quote]quotetest 1[/quote]\r\n" + "[quote=bla]quotetest 2[/quote]\r\n" + "[url]www.skyrimnexus.com[/url]\r\n" + "[url=www.skyrimnexus.com]urltest 2[/url]\r\n" + "[ol]\r\n" + "[li]item 2[/li]" + "[*]item 1\r\n" + "[/ol]\r\n" + "[img]http://www.bbcode.org/images/bbcode_logo.png[/img]\r\n" + "[table][tr][th]headertest1[/th]" + "[th]headertest2[/th][/tr]" + "[tr][td]rowtest11[/td][td]rowtest12[/td][/tr]" + "[tr][td]rowtest21[/td][td]rowtest22[/td][/tr][/table]" + "[email=\"sherb@gmx.net\"]mail me[/email]"; + ui->descriptionView->setHtml(BBCode::convertToHTML(input));*/ + + QString descriptionAsHTML = + QString("" + "" + "%1" + "").arg(BBCode::convertToHTML(nexusDescription)); + + ui->descriptionView->page()->setHtml(descriptionAsHTML); + + // QString descriptionAsHTML = BBCode::convertToHTML(result["description"].toString()); + // ui->descriptionView->setHtml(descriptionAsHTML); + } else { + // ui->descriptionView->setHtml(result["summary"].toString().append(QString("\r\n") + tr("(description incomplete, please visit nexus)"))); + ui->descriptionView->page()->setHtml(tr("(description incomplete, please visit nexus)")); + } + + updateVersionColor(); + } +} + + +void ModInfoDialog::activateNexusTab() +{ + QLineEdit *modIDEdit = findChild("modIDEdit"); + int modID = modIDEdit->text().toInt(); + if (modID != 0) { + QString nexusLink = NexusInterface::instance(m_PluginContainer)->getModURL(modID, m_ModInfo->getGameName()); + QLabel *visitNexusLabel = findChild("visitNexusLabel"); + visitNexusLabel->setText(tr("Visit on Nexus").arg(nexusLink)); + visitNexusLabel->setToolTip(nexusLink); + + if (m_ModInfo->getNexusDescription().isEmpty() || + QDateTime::currentDateTime() > m_ModInfo->getLastNexusQuery().addDays(1)) { + refreshNexusData(modID); + } else { + this->modDetailsUpdated(true); + } + } + QLineEdit *versionEdit = findChild("versionEdit"); + QString currentVersion = m_Settings->value("version", "0.0").toString(); + versionEdit->setText(currentVersion); + ui->customUrlLineEdit->setText(m_ModInfo->getURL()); +} + + +void ModInfoDialog::on_tabWidget_currentChanged(int index) +{ + if (index == TAB_NEXUS || m_RealTabPos[index] == TAB_NEXUS) { + activateNexusTab(); + } +} + + +void ModInfoDialog::on_modIDEdit_editingFinished() +{ + int oldID = m_Settings->value("modid", 0).toInt(); + int modID = ui->modIDEdit->text().toInt(); + if (oldID != modID){ + m_ModInfo->setNexusID(modID); + + ui->descriptionView->page()->setHtml(""); + if (modID != 0) { + m_RequestStarted = false; + refreshNexusData(modID); + } + } +} + +void ModInfoDialog::on_sourceGameEdit_currentIndexChanged(int) +{ + for (auto game : m_PluginContainer->plugins()) { + if (game->gameName() == ui->sourceGameEdit->currentText()) { + m_ModInfo->setGameName(game->gameShortName()); + return; + } + } +} + +void ModInfoDialog::on_versionEdit_editingFinished() +{ + VersionInfo version(ui->versionEdit->text()); + m_ModInfo->setVersion(version); + updateVersionColor(); +} + +void ModInfoDialog::on_customUrlLineEdit_editingFinished() +{ + m_ModInfo->setURL(ui->customUrlLineEdit->text()); +} + +bool ModInfoDialog::recursiveDelete(const QModelIndex &index) +{ + for (int childRow = 0; childRow < m_FileSystemModel->rowCount(index); ++childRow) { + QModelIndex childIndex = m_FileSystemModel->index(childRow, 0, index); + if (m_FileSystemModel->isDir(childIndex)) { + if (!recursiveDelete(childIndex)) { + qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData()); + return false; + } + } else { + if (!m_FileSystemModel->remove(childIndex)) { + qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData()); + return false; + } + } + } + if (!m_FileSystemModel->remove(index)) { + qCritical("failed to delete %s", m_FileSystemModel->fileName(index).toUtf8().constData()); + return false; + } + return true; +} + + +void ModInfoDialog::on_openInExplorerButton_clicked() +{ + ::ShellExecuteW(nullptr, L"explore", ToWString(m_ModInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); +} + +void ModInfoDialog::deleteFile(const QModelIndex &index) +{ + + bool res = m_FileSystemModel->isDir(index) ? recursiveDelete(index) + : m_FileSystemModel->remove(index); + if (!res) { + QString fileName = m_FileSystemModel->fileName(index); + reportError(tr("Failed to delete %1").arg(fileName)); + } +} + +void ModInfoDialog::delete_activated() +{ + if (ui->fileTree->hasFocus()) { + QItemSelectionModel *selection = ui->fileTree->selectionModel(); + + if (selection->hasSelection() && selection->selectedRows().count() >= 1) { + + if (selection->selectedRows().count() == 0) { + return; + } + else if (selection->selectedRows().count() == 1) { + QString fileName = m_FileSystemModel->fileName(selection->selectedRows().at(0)); + if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete \"%1\"?").arg(fileName), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + return; + } + } + else { + if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete the selected files?"), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + return; + } + } + + foreach(QModelIndex index, selection->selectedRows()) { + deleteFile(index); + } + } + } +} + +void ModInfoDialog::deleteTriggered() +{ + if (m_FileSelection.count() == 0) { + return; + } else if (m_FileSelection.count() == 1) { + QString fileName = m_FileSystemModel->fileName(m_FileSelection.at(0)); + if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete \"%1\"?").arg(fileName), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + return; + } + } else { + if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete the selected files?"), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + return; + } + } + + foreach(QModelIndex index, m_FileSelection) { + deleteFile(index); + } +} + + +void ModInfoDialog::renameTriggered() +{ + QModelIndex selection = m_FileSelection.at(0); + QModelIndex index = selection.sibling(selection.row(), 0); + if (!index.isValid() || m_FileSystemModel->isReadOnly()) { + return; + } + + ui->fileTree->edit(index); +} + + +void ModInfoDialog::hideTriggered() +{ + for (QModelIndexList::const_iterator iter = m_FileSelection.constBegin(); + iter != m_FileSelection.constEnd(); ++iter) { + QString path = m_FileSystemModel->filePath(*iter); + if (!path.endsWith(ModInfo::s_HiddenExt)) { + hideFile(path); + } + } +} + + +void ModInfoDialog::unhideTriggered() +{ + for (QModelIndexList::const_iterator iter = m_FileSelection.constBegin(); + iter != m_FileSelection.constEnd(); ++iter) { + QString path = m_FileSystemModel->filePath(*iter); + if (path.endsWith(ModInfo::s_HiddenExt)) { + unhideFile(path); + } + } +} + + +void ModInfoDialog::openFile(const QModelIndex &index) +{ + QString fileName = m_FileSystemModel->filePath(index); + + HINSTANCE res = ::ShellExecuteW(nullptr, L"open", ToWString(fileName).c_str(), nullptr, nullptr, SW_SHOW); + if ((unsigned long long)res <= 32) { + qCritical("failed to invoke %s: %d", qUtf8Printable(fileName), res); + } +} + + +void ModInfoDialog::openTriggered() +{ + foreach(QModelIndex idx, m_FileSelection) { + openFile(idx); + } +} + +void ModInfoDialog::createDirectoryTriggered() +{ + QModelIndex selection = m_FileSelection.at(0); + + QModelIndex index = m_FileSystemModel->isDir(selection) ? selection + : selection.parent(); + index = index.sibling(index.row(), 0); + + QString name = tr("New Folder"); + QString path = m_FileSystemModel->filePath(index).append("/"); + + QModelIndex existingIndex = m_FileSystemModel->index(path + name); + int suffix = 1; + while (existingIndex.isValid()) { + name = tr("New Folder") + QString::number(suffix++); + existingIndex = m_FileSystemModel->index(path + name); + } + + QModelIndex newIndex = m_FileSystemModel->mkdir(index, name); + if (!newIndex.isValid()) { + reportError(tr("Failed to create \"%1\"").arg(name)); + return; + } + + ui->fileTree->setCurrentIndex(newIndex); + ui->fileTree->edit(newIndex); +} + + +void ModInfoDialog::on_fileTree_customContextMenuRequested(const QPoint &pos) +{ + QItemSelectionModel *selectionModel = ui->fileTree->selectionModel(); + m_FileSelection = selectionModel->selectedRows(0); + +// m_FileSelection = ui->fileTree->indexAt(pos); + QMenu menu(ui->fileTree); + + menu.addAction(m_NewFolderAction); + + bool hasFiles = false; + + foreach(QModelIndex idx, m_FileSelection) { + if (m_FileSystemModel->fileInfo(idx).isFile()) { + hasFiles = true; + break; + } + } + + if (selectionModel->hasSelection()) { + if (hasFiles) { + menu.addAction(m_OpenAction); + } + menu.addAction(m_RenameAction); + menu.addAction(m_DeleteAction); + if (m_FileSystemModel->fileName(m_FileSelection.at(0)).endsWith(ModInfo::s_HiddenExt)) { + menu.addAction(m_UnhideAction); + } else { + menu.addAction(m_HideAction); + } + } else { + m_FileSelection.clear(); + m_FileSelection.append(m_FileSystemModel->index(m_FileSystemModel->rootPath(), 0)); + } + menu.exec(ui->fileTree->mapToGlobal(pos)); +} + + +void ModInfoDialog::on_categoriesTree_itemChanged(QTreeWidgetItem *item, int) +{ + QTreeWidgetItem *parent = item->parent(); + while ((parent != nullptr) && ((parent->flags() & Qt::ItemIsUserCheckable) != 0) && (parent->checkState(0) == Qt::Unchecked)) { + parent->setCheckState(0, Qt::Checked); + parent = parent->parent(); + } + refreshPrimaryCategoriesBox(); +} + + +void ModInfoDialog::addCheckedCategories(QTreeWidgetItem *tree) +{ + for (int i = 0; i < tree->childCount(); ++i) { + QTreeWidgetItem *child = tree->child(i); + if (child->checkState(0) == Qt::Checked) { + ui->primaryCategoryBox->addItem(child->text(0), child->data(0, Qt::UserRole)); + addCheckedCategories(child); + } + } +} + + +void ModInfoDialog::refreshPrimaryCategoriesBox() +{ + ui->primaryCategoryBox->clear(); + int primaryCategory = m_ModInfo->getPrimaryCategory(); + addCheckedCategories(ui->categoriesTree->invisibleRootItem()); + for (int i = 0; i < ui->primaryCategoryBox->count(); ++i) { + if (ui->primaryCategoryBox->itemData(i).toInt() == primaryCategory) { + ui->primaryCategoryBox->setCurrentIndex(i); + break; + } + } +} + + +void ModInfoDialog::on_primaryCategoryBox_currentIndexChanged(int index) +{ + if (index != -1) { + m_ModInfo->setPrimaryCategory(ui->primaryCategoryBox->itemData(index).toInt()); + } +} + + +void ModInfoDialog::on_overwriteTree_itemDoubleClicked(QTreeWidgetItem *item, int) +{ + this->close(); + emit modOpen(item->data(1, Qt::UserRole).toString(), TAB_CONFLICTS); +} + + +bool ModInfoDialog::hideFile(const QString &oldName) +{ + QString newName = oldName + ModInfo::s_HiddenExt; + + if (QFileInfo(newName).exists()) { + if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a hidden version of this file. Replace it?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + if (!QFile(newName).remove()) { + QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); + return false; + } + } else { + return false; + } + } + + if (QFile::rename(oldName, newName)) { + return true; + } else { + reportError(tr("failed to rename %1 to %2").arg(oldName).arg(QDir::toNativeSeparators(newName))); + return false; + } +} + + +bool ModInfoDialog::unhideFile(const QString &oldName) +{ + QString newName = oldName.left(oldName.length() - ModInfo::s_HiddenExt.length()); + if (QFileInfo(newName).exists()) { + if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a visible version of this file. Replace it?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + if (!QFile(newName).remove()) { + QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); + return false; + } + } else { + return false; + } + } + if (QFile::rename(oldName, newName)) { + return true; + } else { + reportError(tr("failed to rename %1 to %2").arg(QDir::toNativeSeparators(oldName)).arg(QDir::toNativeSeparators(newName))); + return false; + } +} + + +void ModInfoDialog::hideConflictFile() +{ + if (hideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) { + emit originModified(m_Origin->getID()); + refreshLists(); + } +} + + +void ModInfoDialog::unhideConflictFile() +{ + if (unhideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) { + emit originModified(m_Origin->getID()); + refreshLists(); + } +} + +int ModInfoDialog::getBinaryExecuteInfo(const QFileInfo &targetInfo, QFileInfo &binaryInfo, QString &arguments) +{ + QString extension = targetInfo.suffix(); + if ((extension.compare("cmd", Qt::CaseInsensitive) == 0) || + (extension.compare("com", Qt::CaseInsensitive) == 0) || + (extension.compare("bat", Qt::CaseInsensitive) == 0)) { + binaryInfo = QFileInfo("C:\\Windows\\System32\\cmd.exe"); + arguments = QString("/C \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); + return 1; + } + else if (extension.compare("exe", Qt::CaseInsensitive) == 0) { + binaryInfo = targetInfo; + return 1; + } + else if (extension.compare("jar", Qt::CaseInsensitive) == 0) { + // types that need to be injected into + std::wstring targetPathW = ToWString(targetInfo.absoluteFilePath()); + QString binaryPath; + + { // try to find java automatically + WCHAR buffer[MAX_PATH]; + if (::FindExecutableW(targetPathW.c_str(), nullptr, buffer) > (HINSTANCE)32) { + DWORD binaryType = 0UL; + if (!::GetBinaryTypeW(buffer, &binaryType)) { + qDebug("failed to determine binary type of \"%ls\": %lu", buffer, ::GetLastError()); + } + else if (binaryType == SCS_32BIT_BINARY) { + binaryPath = ToQString(buffer); + } + } + } + if (binaryPath.isEmpty() && (extension == "jar")) { + // second attempt: look to the registry + QSettings javaReg("HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment", QSettings::NativeFormat); + if (javaReg.contains("CurrentVersion")) { + QString currentVersion = javaReg.value("CurrentVersion").toString(); + binaryPath = javaReg.value(QString("%1/JavaHome").arg(currentVersion)).toString().append("\\bin\\javaw.exe"); + } + } + if (binaryPath.isEmpty()) { + binaryPath = QFileDialog::getOpenFileName(this, tr("Select binary"), QString(), tr("Binary") + " (*.exe)"); + } + if (binaryPath.isEmpty()) { + return 0; + } + binaryInfo = QFileInfo(binaryPath); + if (extension == "jar") { + arguments = QString("-jar \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); + } + else { + arguments = QString("\"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); + } + return 1; + } + else { + return 2; + } +} + +void ModInfoDialog::openDataFile() +{ + if (m_ConflictsContextItem != nullptr) { + QFileInfo targetInfo(m_ConflictsContextItem->data(0, Qt::UserRole).toString()); + QFileInfo binaryInfo; + QString arguments; + switch (getBinaryExecuteInfo(targetInfo, binaryInfo, arguments)) { + case 1: { + m_OrganizerCore->spawnBinaryDirect( + binaryInfo, arguments, m_OrganizerCore->currentProfile()->name(), + targetInfo.absolutePath(), "", ""); + } break; + case 2: { + ::ShellExecuteW(nullptr, L"open", + ToWString(targetInfo.absoluteFilePath()).c_str(), + nullptr, nullptr, SW_SHOWNORMAL); + } break; + default: { + // nop + } break; + } + } +} + +void ModInfoDialog::previewDataFile() +{ + QString fileName = QDir::fromNativeSeparators(m_ConflictsContextItem->data(0, Qt::UserRole).toString()); + + // what we have is an absolute path to the file in its actual location (for the primary origin) + // what we want is the path relative to the virtual data directory + + // we need to look in the virtual directory for the file to make sure the info is up to date. + + // check if the file comes from the actual data folder instead of a mod + QDir gameDirectory = m_OrganizerCore->managedGame()->dataDirectory().absolutePath(); + QString relativePath = gameDirectory.relativeFilePath(fileName); + QDir direRelativePath = gameDirectory.relativeFilePath(fileName); + // if the file is on a different drive the dirRelativePath will actually be an absolute path so we make sure that is not the case + if (!direRelativePath.isAbsolute() && !relativePath.startsWith("..")) { + fileName = relativePath; + } + else { + // crude: we search for the next slash after the base mod directory to skip everything up to the data-relative directory + int offset = m_OrganizerCore->settings().getModDirectory().size() + 1; + offset = fileName.indexOf("/", offset); + fileName = fileName.mid(offset + 1); + } + + + + const FileEntry::Ptr file = m_OrganizerCore->directoryStructure()->searchFile(ToWString(fileName), nullptr); + + if (file.get() == nullptr) { + reportError(tr("file not found: %1").arg(qUtf8Printable(fileName))); + return; + } + + // set up preview dialog + PreviewDialog preview(fileName); + auto addFunc = [&](int originId) { + FilesOrigin &origin = m_OrganizerCore->directoryStructure()->getOriginByID(originId); + QString filePath = QDir::fromNativeSeparators(ToQString(origin.getPath())) + "/" + fileName; + if (QFile::exists(filePath)) { + // it's very possible the file doesn't exist, because it's inside an archive. we don't support that + QWidget *wid = m_PluginContainer->previewGenerator().genPreview(filePath); + if (wid == nullptr) { + reportError(tr("failed to generate preview for %1").arg(filePath)); + } + else { + preview.addVariant(ToQString(origin.getName()), wid); + } + } + }; + + addFunc(file->getOrigin()); + for (auto alt : file->getAlternatives()) { + addFunc(alt.first); + } + if (preview.numVariants() > 0) { + preview.exec(); + } + else { + QMessageBox::information(this, tr("Sorry"), tr("Sorry, can't preview anything. This function currently does not support extracting from bsas.")); + } +} + + +void ModInfoDialog::on_overwriteTree_customContextMenuRequested(const QPoint &pos) +{ + m_ConflictsContextItem = ui->overwriteTree->itemAt(pos.x(), pos.y()); + + if (m_ConflictsContextItem != nullptr) { + // offer to hide/unhide file, but not for files from archives + if (!m_ConflictsContextItem->data(1, Qt::UserRole + 2).toBool()) { + QMenu menu; + if (m_ConflictsContextItem->text(0).endsWith(ModInfo::s_HiddenExt)) { + menu.addAction(tr("Un-Hide"), this, SLOT(unhideConflictFile())); + } else { + menu.addAction(tr("Hide"), this, SLOT(hideConflictFile())); + } + + menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile())); + + QString fileName = m_ConflictsContextItem->data(0, Qt::UserRole).toString(); + if (m_PluginContainer->previewGenerator().previewSupported(QFileInfo(fileName).suffix())) { + menu.addAction(tr("Preview"), this, SLOT(previewDataFile())); + } + + menu.exec(ui->overwriteTree->mapToGlobal(pos)); + } + } +} + +void ModInfoDialog::on_overwrittenTree_customContextMenuRequested(const QPoint &pos) +{ + m_ConflictsContextItem = ui->overwrittenTree->itemAt(pos.x(), pos.y()); + + if (m_ConflictsContextItem != nullptr) { + if (!m_ConflictsContextItem->data(1, Qt::UserRole + 2).toBool()) { + QMenu menu; + + menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile())); + + QString fileName = m_ConflictsContextItem->data(0, Qt::UserRole).toString(); + if (m_PluginContainer->previewGenerator().previewSupported(QFileInfo(fileName).suffix())) { + menu.addAction(tr("Preview"), this, SLOT(previewDataFile())); + } + + menu.exec(ui->overwrittenTree->mapToGlobal(pos)); + } + } +} + + +void ModInfoDialog::on_overwrittenTree_itemDoubleClicked(QTreeWidgetItem *item, int) +{ + emit modOpen(item->data(1, Qt::UserRole).toString(), TAB_CONFLICTS); + this->accept(); +} + +void ModInfoDialog::on_refreshButton_clicked() +{ + m_ModInfo->updateNXMInfo(); + + MessageDialog::showMessage(tr("Info requested, please wait"), this); +} + +void ModInfoDialog::on_endorseBtn_clicked() +{ + emit endorseMod(m_ModInfo); +} + +void ModInfoDialog::on_nextButton_clicked() +{ + int currentTab = ui->tabWidget->currentIndex(); + int tab = m_RealTabPos[currentTab]; + + emit modOpenNext(tab); + this->accept(); +} + +void ModInfoDialog::on_prevButton_clicked() +{ + int currentTab = ui->tabWidget->currentIndex(); + int tab = m_RealTabPos[currentTab]; + + emit modOpenPrev(tab); + this->accept(); +} + + +void ModInfoDialog::createTweak() +{ + QString name = QInputDialog::getText(this, tr("Name"), tr("Please enter a name")); + if (name.isNull()) { + return; + } else if (!fixDirectoryName(name)) { + QMessageBox::critical(this, tr("Error"), tr("Invalid name. Must be a valid file name")); + return; + } else if (ui->iniTweaksList->findItems(name, Qt::MatchFixedString).count() != 0) { + QMessageBox::critical(this, tr("Error"), tr("A tweak by that name exists")); + return; + } + + QListWidgetItem *newTweak = new QListWidgetItem(name + ".ini"); + newTweak->setData(Qt::UserRole, "INI Tweaks/" + name + ".ini"); + newTweak->setFlags(newTweak->flags() | Qt::ItemIsUserCheckable); + newTweak->setCheckState(Qt::Unchecked); + ui->iniTweaksList->addItem(newTweak); +} + +void ModInfoDialog::on_iniTweaksList_customContextMenuRequested(const QPoint &pos) +{ + QMenu menu; + menu.addAction(tr("Create Tweak"), this, SLOT(createTweak())); + menu.exec(ui->iniTweaksList->mapToGlobal(pos)); +} diff --git a/src/modinfoforeign.h b/src/modinfoforeign.h index 45fe7689..20bfab2a 100644 --- a/src/modinfoforeign.h +++ b/src/modinfoforeign.h @@ -31,6 +31,7 @@ public: virtual void setNeverEndorse() {} virtual bool remove() { return false; } virtual void endorse(bool) {} + virtual void parseNexusInfo() {} virtual bool isEmpty() const { return false; } virtual QString name() const; virtual QString internalName() const { return name(); } diff --git a/src/modinfooverwrite.h b/src/modinfooverwrite.h index cfd662af..b68fb15b 100644 --- a/src/modinfooverwrite.h +++ b/src/modinfooverwrite.h @@ -33,6 +33,7 @@ public: virtual void setNeverEndorse() {} virtual bool remove() { return false; } virtual void endorse(bool) {} + virtual void parseNexusInfo() {} virtual bool alwaysEnabled() const { return true; } virtual bool isEmpty() const; virtual QString name() const { return "Overwrite"; } diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 3523feae..71b99e10 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -56,7 +56,7 @@ ModInfoRegular::ModInfoRegular(PluginContainer *pluginContainer, const IPluginGa connect(&m_NexusBridge, SIGNAL(endorsementToggled(QString,int,QVariant,QVariant)) , this, SLOT(nxmEndorsementToggled(QString,int,QVariant,QVariant))); connect(&m_NexusBridge, SIGNAL(requestFailed(QString,int,int,QVariant,QString)) - , this, SLOT(nxmRequestFailed(QString,int,int,QVariant,QString))); + , this, SLOT(nxmRequestFailed(QString,int,int,QVariant, QNetworkReply::NetworkError,QString))); } @@ -213,13 +213,17 @@ bool ModInfoRegular::downgradeAvailable() const void ModInfoRegular::nxmDescriptionAvailable(QString, int, QVariant, QVariant resultData) { QVariantMap result = resultData.toMap(); - setNewestVersion(VersionInfo(result["version"].toString())); setNexusDescription(result["description"].toString()); if ((m_EndorsedState != ENDORSED_NEVER) && (result.contains("endorsement"))) { QVariantMap endorsement = result["endorsement"].toMap(); QString endorsementStatus = endorsement["endorse_status"].toString(); - setEndorsedState(endorsementStatus.compare("Endorsed") == 0 ? ENDORSED_TRUE : ENDORSED_FALSE); + if (endorsementStatus.compare("Endorsed") == 00) + setEndorsedState(ENDORSED_TRUE); + else if (endorsementStatus.compare("Abstained") == 00) + setEndorsedState(ENDORSED_NEVER); + else + setEndorsedState(ENDORSED_FALSE); } m_LastNexusQuery = QDateTime::currentDateTime(); //m_MetaInfoChanged = true; @@ -234,6 +238,8 @@ void ModInfoRegular::nxmEndorsementToggled(QString, int, QVariant, QVariant resu if (results["code"].toInt() == 200 || results["code"].toInt() == 201) { if (results["status"].toString().compare("Endorsed") == 0) { m_EndorsedState = ENDORSED_TRUE; + } else if (results["status"].toString().compare("Abstained") == 0) { + m_EndorsedState = ENDORSED_NEVER; } else { m_EndorsedState = ENDORSED_FALSE; } @@ -244,7 +250,7 @@ void ModInfoRegular::nxmEndorsementToggled(QString, int, QVariant, QVariant resu } -void ModInfoRegular::nxmRequestFailed(QString, int, int, QVariant userData, const QString &errorMessage) +void ModInfoRegular::nxmRequestFailed(QString, int, int, QVariant userData, QNetworkReply::NetworkError error, const QString &errorMessage) { QString fullMessage = errorMessage; if (userData.canConvert() && (userData.toInt() == 1)) { diff --git a/src/modinforegular.h b/src/modinforegular.h index 093b4e5b..fdb0e672 100644 --- a/src/modinforegular.h +++ b/src/modinforegular.h @@ -353,7 +353,7 @@ private slots: void nxmDescriptionAvailable(QString, int modID, QVariant userData, QVariant resultData); void nxmEndorsementToggled(QString, int, QVariant userData, QVariant resultData); - void nxmRequestFailed(QString, int modID, int fileID, QVariant userData, const QString &errorMessage); + void nxmRequestFailed(QString, int modID, int fileID, QVariant userData, QNetworkReply::NetworkError error, const QString &errorMessage); protected: diff --git a/src/modinfoseparator.h b/src/modinfoseparator.h index f215fb17..c5e0f0e5 100644 --- a/src/modinfoseparator.h +++ b/src/modinfoseparator.h @@ -20,21 +20,15 @@ public: virtual int getNexusID() const { return -1; } - virtual void setGameName(QString /*gameName*/) - { - } + virtual void setGameName(QString /*gameName*/) {} - virtual void setNexusID(int /*modID*/) - { - } + virtual void setNexusID(int /*modID*/) {} - virtual void endorse(bool /*doEndorse*/) - { - } + virtual void endorse(bool /*doEndorse*/) {} - virtual void ignoreUpdate(bool /*ignore*/) - { - } + virtual void parseNexusInfo() {} + + virtual void ignoreUpdate(bool /*ignore*/) {} virtual bool canBeUpdated() const { return false; } virtual bool canBeEnabled() const { return false; } diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 4fe86136..789d30d2 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -1,734 +1,710 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "nexusinterface.h" - -#include "iplugingame.h" -#include "nxmaccessmanager.h" -#include "json.h" -#include "selectiondialog.h" -#include -#include - -#include -#include -#include - -#include - - -using namespace MOBase; -using namespace MOShared; - - -NexusBridge::NexusBridge(PluginContainer *pluginContainer, const QString &subModule) - : m_Interface(NexusInterface::instance(pluginContainer)) - , m_SubModule(subModule) -{ -} - -void NexusBridge::requestDescription(QString gameName, int modID, QVariant userData) -{ - m_RequestIDs.insert(m_Interface->requestDescription(gameName, modID, this, userData, m_SubModule)); -} - -void NexusBridge::requestFiles(QString gameName, int modID, QVariant userData) -{ - m_RequestIDs.insert(m_Interface->requestFiles(gameName, modID, this, userData, m_SubModule)); -} - -void NexusBridge::requestFileInfo(QString gameName, int modID, int fileID, QVariant userData) -{ - m_RequestIDs.insert(m_Interface->requestFileInfo(gameName, modID, fileID, this, userData, m_SubModule)); -} - -void NexusBridge::requestDownloadURL(QString gameName, int modID, int fileID, QVariant userData) -{ - m_RequestIDs.insert(m_Interface->requestDownloadURL(gameName, modID, fileID, this, userData, m_SubModule)); -} - -void NexusBridge::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData) -{ - m_RequestIDs.insert(m_Interface->requestToggleEndorsement(gameName, modID, modVersion, endorse, this, userData, m_SubModule)); -} - -void NexusBridge::nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - - emit descriptionAvailable(gameName, modID, userData, resultData); - } -} - -void NexusBridge::nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - - QList fileInfoList; - - QVariantMap resultInfo = resultData.toMap(); - QList resultList = resultInfo["files"].toList(); - - for (const QVariant &file : resultList) { - ModRepositoryFileInfo temp; - QVariantMap fileInfo = file.toMap(); - temp.uri = fileInfo["file_name"].toString(); - temp.name = fileInfo["name"].toString(); - temp.description = fileInfo["changelog_html"].toString(); - temp.version = VersionInfo(fileInfo["version"].toString()); - temp.categoryID = fileInfo["category_id"].toInt(); - temp.fileID = fileInfo["file_id"].toInt(); - temp.fileSize = fileInfo["size"].toInt(); - fileInfoList.append(temp); - } - - emit filesAvailable(gameName, modID, userData, fileInfoList); - } -} - -void NexusBridge::nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - emit fileInfoAvailable(gameName, modID, fileID, userData, resultData); - } -} - -void NexusBridge::nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - emit downloadURLsAvailable(gameName, modID, fileID, userData, resultData); - } -} - -void NexusBridge::nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - emit endorsementToggled(gameName, modID, userData, resultData); - } -} - -void NexusBridge::nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, const QString &errorMessage) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - emit requestFailed(gameName, modID, fileID, userData, errorMessage); - } -} - - -QAtomicInt NexusInterface::NXMRequestInfo::s_NextID(0); - - -NexusInterface::NexusInterface(PluginContainer *pluginContainer) - : m_NMMVersion(), m_PluginContainer(pluginContainer) -{ - m_MOVersion = createVersionInfo(); - - m_AccessManager = new NXMAccessManager(this, m_MOVersion.displayString(3)); - m_DiskCache = new QNetworkDiskCache(this); - connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString))); -} - -NXMAccessManager *NexusInterface::getAccessManager() -{ - return m_AccessManager; -} - -NexusInterface::~NexusInterface() -{ - cleanup(); -} - -NexusInterface *NexusInterface::instance(PluginContainer *pluginContainer) -{ - static NexusInterface s_Instance(pluginContainer); - return &s_Instance; -} - -void NexusInterface::setCacheDirectory(const QString &directory) -{ - m_DiskCache->setCacheDirectory(directory); - m_AccessManager->setCache(m_DiskCache); -} - -void NexusInterface::setNMMVersion(const QString &nmmVersion) -{ - m_NMMVersion = nmmVersion; - m_AccessManager->setNMMVersion(nmmVersion); -} - -void NexusInterface::loginCompleted() -{ - nextRequest(); -} - - -void NexusInterface::interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query) -{ - //Look for something along the lines of modulename-Vn-m + any old rubbish. - static std::regex exp(R"exp(^([a-zA-Z0-9_'"\-.() ]*?)([-_ ][VvRr]?[0-9_]+)?-([1-9][0-9]*).*\.(zip|rar|7z))exp"); - static std::regex simpleexp("^([a-zA-Z0-9_]+)"); - - QByteArray fileNameUTF8 = fileName.toUtf8(); - std::cmatch result; - if (std::regex_search(fileNameUTF8.constData(), result, exp)) { - modName = QString::fromUtf8(result[1].str().c_str()); - modName = modName.replace('_', ' ').trimmed(); - - std::string candidate = result[3].str(); - std::string candidate2 = result[2].str(); - if (candidate2.length() != 0 && (candidate2.find_last_of("VvRr") == std::string::npos)) { - // well, that second match might be an id too... - size_t offset = strspn(candidate2.c_str(), "-_ "); - if (offset < candidate2.length() && query) { - SelectionDialog selection(tr("Failed to guess mod id for \"%1\", please pick the correct one").arg(fileName)); - QString r2Highlight(fileName); - r2Highlight.insert(result.position(2) + result.length(2), "* ") - .insert(result.position(2) + static_cast(offset), " *"); - QString r3Highlight(fileName); - r3Highlight.insert(result.position(3) + result.length(3), "* ").insert(result.position(3), " *"); - - selection.addChoice(candidate.c_str(), r3Highlight, static_cast(strtol(candidate.c_str(), nullptr, 10))); - selection.addChoice(candidate2.c_str() + offset, r2Highlight, static_cast(abs(strtol(candidate2.c_str() + offset, nullptr, 10)))); - if (selection.exec() == QDialog::Accepted) { - modID = selection.getChoiceData().toInt(); - } else { - modID = -1; - } - } else { - modID = -1; - } - } else { - modID = strtol(candidate.c_str(), nullptr, 10); - } - qDebug("mod id guessed: %s -> %d", qUtf8Printable(fileName), modID); - } else if (std::regex_search(fileNameUTF8.constData(), result, simpleexp)) { - qDebug("simple expression matched, using name only"); - modName = QString::fromUtf8(result[1].str().c_str()); - modName = modName.replace('_', ' ').trimmed(); - - modID = -1; - } else { - qDebug("no expression matched!"); - modName.clear(); - modID = -1; - } -} - -bool NexusInterface::isURLGameRelated(const QUrl &url) const -{ - QString const name(url.toString()); - return name.startsWith(getGameURL("") + "/") || - name.startsWith(getOldModsURL("") + "/"); -} - -QString NexusInterface::getGameURL(QString gameName) const -{ - IPluginGame *game = getGame(gameName); - return "https://www.nexusmods.com/" + game->gameNexusName().toLower(); -} - -QString NexusInterface::getOldModsURL(QString gameName) const -{ - IPluginGame *game = getGame(gameName); - return "https://" + game->gameNexusName().toLower() + ".nexusmods.com/mods"; -} - - -QString NexusInterface::getModURL(int modID, QString gameName = "") const -{ - return QString("%1/mods/%2").arg(getGameURL(gameName)).arg(modID); -} - -std::vector> NexusInterface::getGameChoices(const MOBase::IPluginGame *game) -{ - std::vector> choices; - choices.push_back(std::pair(game->gameShortName(), game->gameName())); - for (QString gameName : game->validShortNames()) { - for (auto gamePlugin : m_PluginContainer->plugins()) { - if (gamePlugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { - choices.push_back(std::pair(gamePlugin->gameShortName(), gamePlugin->gameName())); - break; - } - } - } - return choices; -} - -bool NexusInterface::isModURL(int modID, const QString &url) const -{ - if (QUrl(url) == QUrl(getModURL(modID))) { - return true; - } - //Try the alternate (old style) mod name - QString alt = QString("%1/%2").arg(getOldModsURL("")).arg(modID); - return QUrl(alt) == QUrl(url); -} - -void NexusInterface::setPluginContainer(PluginContainer *pluginContainer) -{ - m_PluginContainer = pluginContainer; -} - -int NexusInterface::requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, - const QString &subModule, MOBase::IPluginGame const *game) -{ - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_DESCRIPTION, userData, subModule, game); - m_RequestQueue.enqueue(requestInfo); - - connect(this, SIGNAL(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)), - receiver, SLOT(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - - -int NexusInterface::requestUpdates(const std::vector &modIDs, QObject *receiver, QVariant userData, - QString gameName, const QString &subModule) -{ - IPluginGame *game = getGame(gameName); - NXMRequestInfo requestInfo(modIDs, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, game); - m_RequestQueue.enqueue(requestInfo); - - connect(this, SIGNAL(nxmUpdatesAvailable(std::vector, QVariant, QVariant, int)), - receiver, SLOT(nxmUpdatesAvailable(std::vector, QVariant, QVariant, int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - - -void NexusInterface::fakeFiles() -{ - static int id = 42; - - QVariantList result; - QVariantMap fileMap; - fileMap["uri"] = "fakeURI"; - fileMap["name"] = "fakeName"; - fileMap["description"] = "fakeDescription"; - fileMap["version"] = "1.0.0"; - fileMap["category_id"] = "1"; - fileMap["id"] = "1"; - fileMap["size"] = "512"; - result.append(fileMap); - - emit nxmFilesAvailable("fakeGame", 1234, "fake", result, id++); -} - - -int NexusInterface::requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData, - const QString &subModule, MOBase::IPluginGame const *game) -{ - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_FILES, userData, subModule, game); - m_RequestQueue.enqueue(requestInfo); - connect(this, SIGNAL(nxmFilesAvailable(QString, int, QVariant, QVariant, int)), - receiver, SLOT(nxmFilesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - - -int NexusInterface::requestFileInfo(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule) -{ - IPluginGame *gamePlugin = getGame(gameName); - NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_FILEINFO, userData, subModule, gamePlugin); - m_RequestQueue.enqueue(requestInfo); - - connect(this, SIGNAL(nxmFileInfoAvailable(QString, int, int, QVariant, QVariant, int)), - receiver, SLOT(nxmFileInfoAvailable(QString, int, int, QVariant, QVariant, int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - - -int NexusInterface::requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, - const QString &subModule, MOBase::IPluginGame const *game) -{ - NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_DOWNLOADURL, userData, subModule, game); - m_RequestQueue.enqueue(requestInfo); - - connect(this, SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), - receiver, SLOT(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(QString,int,int,QVariant,int,QString)), - receiver, SLOT(nxmRequestFailed(QString,int,int,QVariant,int,QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - - -int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, - const QString &subModule, MOBase::IPluginGame const *game) -{ - NXMRequestInfo requestInfo(modID, modVersion, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game); - requestInfo.m_Endorse = endorse; - m_RequestQueue.enqueue(requestInfo); - - connect(this, SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), - receiver, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - -bool NexusInterface::requiresLogin(const NXMRequestInfo &info) -{ - return (info.m_Type == NXMRequestInfo::TYPE_TOGGLEENDORSEMENT) - || (info.m_Type == NXMRequestInfo::TYPE_DOWNLOADURL); -} - -IPluginGame* NexusInterface::getGame(QString gameName) const -{ - auto gamePlugins = m_PluginContainer->plugins(); - IPluginGame *gamePlugin = qApp->property("managed_game").value(); - for (auto plugin : gamePlugins) { - if (plugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { - gamePlugin = plugin; - break; - } - } - return gamePlugin; -} - -void NexusInterface::cleanup() -{ -// delete m_AccessManager; -// delete m_DiskCache; - m_AccessManager = nullptr; - m_DiskCache = nullptr; -} - -void NexusInterface::clearCache() -{ - m_DiskCache->clear(); - m_AccessManager->clearCookies(); -} - -void NexusInterface::nextRequest() -{ - if ((m_ActiveRequest.size() >= MAX_ACTIVE_DOWNLOADS) - || m_RequestQueue.isEmpty()) { - return; - } - - if (requiresLogin(m_RequestQueue.head()) && !getAccessManager()->validated()) { - if (!getAccessManager()->validateAttempted()) { - emit needLogin(); - return; - } else if (getAccessManager()->validateWaiting()) { - return; - } - } - - NXMRequestInfo info = m_RequestQueue.dequeue(); - info.m_Timeout = new QTimer(this); - info.m_Timeout->setInterval(60000); - - QJsonObject postObject; - QJsonDocument postData(postObject); - - QString url; - if (!info.m_Reroute) { - bool hasParams = false; - switch (info.m_Type) { - case NXMRequestInfo::TYPE_DESCRIPTION: { - url = QString("%1/games/%2/mods/%3").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); - } break; - case NXMRequestInfo::TYPE_FILES: { - url = QString("%1/games/%2/mods/%3/files").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); - } break; - case NXMRequestInfo::TYPE_FILEINFO: { - url = QString("%1/games/%2/mods/%3/files/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); - } break; - case NXMRequestInfo::TYPE_DOWNLOADURL: { - url = QString("%1/games/%2/mods/%3/files/%4/download_link").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); - } break; - case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { - QString endorse = info.m_Endorse ? "endorse" : "abstain"; - url = QString("%1/games/%2/mods/%3/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(endorse); - postObject.insert("Version", info.m_ModVersion); - postData.setObject(postObject); - } break; - case NXMRequestInfo::TYPE_GETUPDATES: { - QString modIDList = VectorJoin(info.m_ModIDList, ","); - modIDList = "[" + modIDList + "]"; - url = QString("%1/Mods/GetUpdates?ModList=%2").arg(info.m_URL).arg(modIDList); - } break; - } - } else { - url = info.m_URL; - } - QNetworkRequest request(url); - request.setRawHeader("apikey", m_AccessManager->apiKey().toUtf8()); - request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m_AccessManager->userAgent(info.m_SubModule)); - request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - request.setRawHeader("Protocol-Version", "0.5.5"); - request.setRawHeader("Application-Version", QApplication::applicationVersion().toUtf8()); - - if (postData.object().isEmpty()) - info.m_Reply = m_AccessManager->get(request); - else - info.m_Reply = m_AccessManager->post(request, postData.toJson()); - - connect(info.m_Reply, SIGNAL(finished()), this, SLOT(requestFinished())); - connect(info.m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); - connect(info.m_Timeout, SIGNAL(timeout()), this, SLOT(requestTimeout())); - info.m_Timeout->start(); - m_ActiveRequest.push_back(info); -} - - -void NexusInterface::downloadRequestedNXM(const QString &url) -{ - emit requestNXMDownload(url); -} - -void NexusInterface::requestFinished(std::list::iterator iter) -{ - QNetworkReply *reply = iter->m_Reply; - - if (reply->error() != QNetworkReply::NoError) { - qWarning("request failed: %s", reply->errorString().toUtf8().constData()); - emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->errorString()); - } else { - int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (statusCode == 301) { - // redirect request, return request to queue - iter->m_URL = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toString(); - iter->m_Reroute = true; - m_RequestQueue.enqueue(*iter); - //nextRequest(); - return; - } - QByteArray data = reply->readAll(); - if (data.isNull() || data.isEmpty() || (strcmp(data.constData(), "null") == 0)) { - QString nexusError(reply->rawHeader("NexusErrorInfo")); - if (nexusError.length() == 0) { - nexusError = tr("empty response"); - } - qDebug("nexus error: %s", qUtf8Printable(nexusError)); - emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, nexusError); - } else { - bool ok; - QVariant result = QtJson::parse(data, ok); - if (result.isValid() && ok) { - switch (iter->m_Type) { - case NXMRequestInfo::TYPE_DESCRIPTION: { - emit nxmDescriptionAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); - } break; - case NXMRequestInfo::TYPE_FILES: { - emit nxmFilesAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); - } break; - case NXMRequestInfo::TYPE_FILEINFO: { - emit nxmFileInfoAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID); - } break; - case NXMRequestInfo::TYPE_DOWNLOADURL: { - emit nxmDownloadURLsAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID); - } break; - case NXMRequestInfo::TYPE_GETUPDATES: { - emit nxmUpdatesAvailable(iter->m_ModIDList, iter->m_UserData, result, iter->m_ID); - } break; - case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { - emit nxmEndorsementToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); - } break; - } - } else { - emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, tr("invalid response")); - } - } - } -} - - -void NexusInterface::requestFinished() -{ - QNetworkReply *reply = static_cast(sender()); - for (std::list::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) { - if (iter->m_Reply == reply) { - iter->m_Timeout->stop(); - iter->m_Timeout->deleteLater(); - requestFinished(iter); - iter->m_Reply->deleteLater(); - m_ActiveRequest.erase(iter); - nextRequest(); - return; - } - } -} - - -void NexusInterface::requestError(QNetworkReply::NetworkError) -{ - QNetworkReply *reply = qobject_cast(sender()); - if (reply == nullptr) { - qWarning("invalid sender type"); - return; - } - - qCritical("request (%s) error: %s (%d)", - qUtf8Printable(reply->url().toString()), - qUtf8Printable(reply->errorString()), - reply->error()); -} - - -void NexusInterface::requestTimeout() -{ - QTimer *timer = qobject_cast(sender()); - if (timer == nullptr) { - qWarning("invalid sender type"); - return; - } - for (std::list::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) { - if (iter->m_Timeout == timer) { - // this abort causes a "request failed" which cleans up the rest - iter->m_Reply->abort(); - return; - } - } -} - -namespace { - QString get_management_url(MOBase::IPluginGame const *game) - { - return "https://api.nexusmods.com/v1"; - } -} - -NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID - , NexusInterface::NXMRequestInfo::Type type - , QVariant userData - , const QString &subModule - , MOBase::IPluginGame const *game - ) - : m_ModID(modID) - , m_ModVersion("0") - , m_FileID(0) - , m_Reply(nullptr) - , m_Type(type) - , m_UserData(userData) - , m_Timeout(nullptr) - , m_Reroute(false) - , m_ID(s_NextID.fetchAndAddAcquire(1)) - , m_URL(get_management_url(game)) - , m_SubModule(subModule) - , m_NexusGameID(game->nexusGameID()) - , m_GameName(game->gameNexusName()) - , m_Endorse(false) -{} - -NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID - , QString modVersion - , NexusInterface::NXMRequestInfo::Type type - , QVariant userData - , const QString &subModule - , MOBase::IPluginGame const *game -) - : m_ModID(modID) - , m_ModVersion(modVersion) - , m_FileID(0) - , m_Reply(nullptr) - , m_Type(type) - , m_UserData(userData) - , m_Timeout(nullptr) - , m_Reroute(false) - , m_ID(s_NextID.fetchAndAddAcquire(1)) - , m_URL(get_management_url(game)) - , m_SubModule(subModule) - , m_NexusGameID(game->nexusGameID()) - , m_GameName(game->gameNexusName()) - , m_Endorse(false) -{} - -NexusInterface::NXMRequestInfo::NXMRequestInfo(std::vector modIDList - , NexusInterface::NXMRequestInfo::Type type - , QVariant userData - , const QString &subModule - , MOBase::IPluginGame const *game - ) - : m_ModID(-1) - , m_ModVersion("0") - , m_ModIDList(modIDList) - , m_FileID(0) - , m_Reply(nullptr) - , m_Type(type) - , m_UserData(userData) - , m_Timeout(nullptr) - , m_Reroute(false) - , m_ID(s_NextID.fetchAndAddAcquire(1)) - , m_URL(get_management_url(game)) - , m_SubModule(subModule) - , m_NexusGameID(game->nexusGameID()) - , m_GameName(game->gameNexusName()) - , m_Endorse(false) -{} - -NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID - , int fileID - , NexusInterface::NXMRequestInfo::Type type - , QVariant userData - , const QString &subModule - , MOBase::IPluginGame const *game -) - : m_ModID(modID) - , m_ModVersion("0") - , m_FileID(fileID) - , m_Reply(nullptr) - , m_Type(type) - , m_UserData(userData) - , m_Timeout(nullptr) - , m_Reroute(false) - , m_ID(s_NextID.fetchAndAddAcquire(1)) - , m_URL(get_management_url(game)) - , m_SubModule(subModule) - , m_NexusGameID(game->nexusGameID()) - , m_GameName(game->gameNexusName()) - , m_Endorse(false) -{} +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "nexusinterface.h" + +#include "iplugingame.h" +#include "nxmaccessmanager.h" +#include "json.h" +#include "selectiondialog.h" +#include "bbcode.h" +#include +#include + +#include +#include +#include + +#include + + +using namespace MOBase; +using namespace MOShared; + + +NexusBridge::NexusBridge(PluginContainer *pluginContainer, const QString &subModule) + : m_Interface(NexusInterface::instance(pluginContainer)) + , m_SubModule(subModule) +{ +} + +void NexusBridge::requestDescription(QString gameName, int modID, QVariant userData) +{ + m_RequestIDs.insert(m_Interface->requestDescription(gameName, modID, this, userData, m_SubModule)); +} + +void NexusBridge::requestFiles(QString gameName, int modID, QVariant userData) +{ + m_RequestIDs.insert(m_Interface->requestFiles(gameName, modID, this, userData, m_SubModule)); +} + +void NexusBridge::requestFileInfo(QString gameName, int modID, int fileID, QVariant userData) +{ + m_RequestIDs.insert(m_Interface->requestFileInfo(gameName, modID, fileID, this, userData, m_SubModule)); +} + +void NexusBridge::requestDownloadURL(QString gameName, int modID, int fileID, QVariant userData) +{ + m_RequestIDs.insert(m_Interface->requestDownloadURL(gameName, modID, fileID, this, userData, m_SubModule)); +} + +void NexusBridge::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData) +{ + m_RequestIDs.insert(m_Interface->requestToggleEndorsement(gameName, modID, modVersion, endorse, this, userData, m_SubModule)); +} + +void NexusBridge::nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + + emit descriptionAvailable(gameName, modID, userData, resultData); + } +} + +void NexusBridge::nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + + QList fileInfoList; + + QVariantMap resultInfo = resultData.toMap(); + QList resultList = resultInfo["files"].toList(); + + for (const QVariant &file : resultList) { + ModRepositoryFileInfo temp; + QVariantMap fileInfo = file.toMap(); + temp.uri = fileInfo["file_name"].toString(); + temp.name = fileInfo["name"].toString(); + temp.description = BBCode::convertToHTML(fileInfo["changelog_html"].toString()); + temp.version = VersionInfo(fileInfo["version"].toString()); + temp.categoryID = fileInfo["category_id"].toInt(); + temp.fileID = fileInfo["file_id"].toInt(); + temp.fileSize = fileInfo["size"].toInt(); + fileInfoList.append(temp); + } + + emit filesAvailable(gameName, modID, userData, fileInfoList); + } +} + +void NexusBridge::nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit fileInfoAvailable(gameName, modID, fileID, userData, resultData); + } +} + +void NexusBridge::nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit downloadURLsAvailable(gameName, modID, fileID, userData, resultData); + } +} + +void NexusBridge::nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit endorsementToggled(gameName, modID, userData, resultData); + } +} + +void NexusBridge::nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorMessage) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit requestFailed(gameName, modID, fileID, userData, errorMessage); + } +} + + +QAtomicInt NexusInterface::NXMRequestInfo::s_NextID(0); + + +NexusInterface::NexusInterface(PluginContainer *pluginContainer) + : m_NMMVersion(), m_PluginContainer(pluginContainer) +{ + m_MOVersion = createVersionInfo(); + + m_AccessManager = new NXMAccessManager(this, m_MOVersion.displayString(3)); + m_DiskCache = new QNetworkDiskCache(this); + connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString))); +} + +NXMAccessManager *NexusInterface::getAccessManager() +{ + return m_AccessManager; +} + +NexusInterface::~NexusInterface() +{ + cleanup(); +} + +NexusInterface *NexusInterface::instance(PluginContainer *pluginContainer) +{ + static NexusInterface s_Instance(pluginContainer); + return &s_Instance; +} + +void NexusInterface::setCacheDirectory(const QString &directory) +{ + m_DiskCache->setCacheDirectory(directory); + m_AccessManager->setCache(m_DiskCache); +} + +void NexusInterface::setNMMVersion(const QString &nmmVersion) +{ + m_NMMVersion = nmmVersion; + m_AccessManager->setNMMVersion(nmmVersion); +} + +void NexusInterface::loginCompleted() +{ + nextRequest(); +} + + +void NexusInterface::interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query) +{ + //Look for something along the lines of modulename-Vn-m + any old rubbish. + static std::regex exp(R"exp(^([a-zA-Z0-9_'"\-.() ]*?)([-_ ][VvRr]?[0-9_]+)?-([1-9][0-9]*).*\.(zip|rar|7z))exp"); + static std::regex simpleexp("^([a-zA-Z0-9_]+)"); + + QByteArray fileNameUTF8 = fileName.toUtf8(); + std::cmatch result; + if (std::regex_search(fileNameUTF8.constData(), result, exp)) { + modName = QString::fromUtf8(result[1].str().c_str()); + modName = modName.replace('_', ' ').trimmed(); + + std::string candidate = result[3].str(); + std::string candidate2 = result[2].str(); + if (candidate2.length() != 0 && (candidate2.find_last_of("VvRr") == std::string::npos)) { + // well, that second match might be an id too... + size_t offset = strspn(candidate2.c_str(), "-_ "); + if (offset < candidate2.length() && query) { + SelectionDialog selection(tr("Failed to guess mod id for \"%1\", please pick the correct one").arg(fileName)); + QString r2Highlight(fileName); + r2Highlight.insert(result.position(2) + result.length(2), "* ") + .insert(result.position(2) + static_cast(offset), " *"); + QString r3Highlight(fileName); + r3Highlight.insert(result.position(3) + result.length(3), "* ").insert(result.position(3), " *"); + + selection.addChoice(candidate.c_str(), r3Highlight, static_cast(strtol(candidate.c_str(), nullptr, 10))); + selection.addChoice(candidate2.c_str() + offset, r2Highlight, static_cast(abs(strtol(candidate2.c_str() + offset, nullptr, 10)))); + if (selection.exec() == QDialog::Accepted) { + modID = selection.getChoiceData().toInt(); + } else { + modID = -1; + } + } else { + modID = -1; + } + } else { + modID = strtol(candidate.c_str(), nullptr, 10); + } + qDebug("mod id guessed: %s -> %d", qUtf8Printable(fileName), modID); + } else if (std::regex_search(fileNameUTF8.constData(), result, simpleexp)) { + qDebug("simple expression matched, using name only"); + modName = QString::fromUtf8(result[1].str().c_str()); + modName = modName.replace('_', ' ').trimmed(); + + modID = -1; + } else { + qDebug("no expression matched!"); + modName.clear(); + modID = -1; + } +} + +bool NexusInterface::isURLGameRelated(const QUrl &url) const +{ + QString const name(url.toString()); + return name.startsWith(getGameURL("") + "/") || + name.startsWith(getOldModsURL("") + "/"); +} + +QString NexusInterface::getGameURL(QString gameName) const +{ + IPluginGame *game = getGame(gameName); + return "https://www.nexusmods.com/" + game->gameNexusName().toLower(); +} + +QString NexusInterface::getOldModsURL(QString gameName) const +{ + IPluginGame *game = getGame(gameName); + return "https://" + game->gameNexusName().toLower() + ".nexusmods.com/mods"; +} + + +QString NexusInterface::getModURL(int modID, QString gameName = "") const +{ + return QString("%1/mods/%2").arg(getGameURL(gameName)).arg(modID); +} + +std::vector> NexusInterface::getGameChoices(const MOBase::IPluginGame *game) +{ + std::vector> choices; + choices.push_back(std::pair(game->gameShortName(), game->gameName())); + for (QString gameName : game->validShortNames()) { + for (auto gamePlugin : m_PluginContainer->plugins()) { + if (gamePlugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { + choices.push_back(std::pair(gamePlugin->gameShortName(), gamePlugin->gameName())); + break; + } + } + } + return choices; +} + +bool NexusInterface::isModURL(int modID, const QString &url) const +{ + if (QUrl(url) == QUrl(getModURL(modID))) { + return true; + } + //Try the alternate (old style) mod name + QString alt = QString("%1/%2").arg(getOldModsURL("")).arg(modID); + return QUrl(alt) == QUrl(url); +} + +void NexusInterface::setPluginContainer(PluginContainer *pluginContainer) +{ + m_PluginContainer = pluginContainer; +} + +int NexusInterface::requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, + const QString &subModule, MOBase::IPluginGame const *game) +{ + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_DESCRIPTION, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + + +int NexusInterface::requestUpdates(const int &modID, QObject *receiver, QVariant userData, + QString gameName, const QString &subModule) +{ + IPluginGame *game = getGame(gameName); + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + + +void NexusInterface::fakeFiles() +{ + static int id = 42; + + QVariantList result; + QVariantMap fileMap; + fileMap["uri"] = "fakeURI"; + fileMap["name"] = "fakeName"; + fileMap["description"] = "fakeDescription"; + fileMap["version"] = "1.0.0"; + fileMap["category_id"] = "1"; + fileMap["id"] = "1"; + fileMap["size"] = "512"; + result.append(fileMap); + + emit nxmFilesAvailable("fakeGame", 1234, "fake", result, id++); +} + + +int NexusInterface::requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData, + const QString &subModule, MOBase::IPluginGame const *game) +{ + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_FILES, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); + connect(this, SIGNAL(nxmFilesAvailable(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmFilesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + + +int NexusInterface::requestFileInfo(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule) +{ + IPluginGame *gamePlugin = getGame(gameName); + NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_FILEINFO, userData, subModule, gamePlugin); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmFileInfoAvailable(QString, int, int, QVariant, QVariant, int)), + receiver, SLOT(nxmFileInfoAvailable(QString, int, int, QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + + +int NexusInterface::requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, + const QString &subModule, MOBase::IPluginGame const *game) +{ + NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_DOWNLOADURL, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), + receiver, SLOT(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString,int,int,QVariant,int,QNetworkReply::NetworkError,QString)), + receiver, SLOT(nxmRequestFailed(QString,int,int,QVariant,int,QNetworkReply::NetworkError,QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + + +int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, + const QString &subModule, MOBase::IPluginGame const *game) +{ + NXMRequestInfo requestInfo(modID, modVersion, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game); + requestInfo.m_Endorse = endorse; + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + +bool NexusInterface::requiresLogin(const NXMRequestInfo &info) +{ + return (info.m_Type == NXMRequestInfo::TYPE_TOGGLEENDORSEMENT) + || (info.m_Type == NXMRequestInfo::TYPE_DOWNLOADURL); +} + +IPluginGame* NexusInterface::getGame(QString gameName) const +{ + auto gamePlugins = m_PluginContainer->plugins(); + IPluginGame *gamePlugin = qApp->property("managed_game").value(); + for (auto plugin : gamePlugins) { + if (plugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { + gamePlugin = plugin; + break; + } + } + return gamePlugin; +} + +void NexusInterface::cleanup() +{ +// delete m_AccessManager; +// delete m_DiskCache; + m_AccessManager = nullptr; + m_DiskCache = nullptr; +} + +void NexusInterface::clearCache() +{ + m_DiskCache->clear(); + m_AccessManager->clearCookies(); +} + +void NexusInterface::nextRequest() +{ + if ((m_ActiveRequest.size() >= MAX_ACTIVE_DOWNLOADS) + || m_RequestQueue.isEmpty()) { + return; + } + + if (requiresLogin(m_RequestQueue.head()) && !getAccessManager()->validated()) { + if (!getAccessManager()->validateAttempted()) { + emit needLogin(); + return; + } else if (getAccessManager()->validateWaiting()) { + return; + } + } + + NXMRequestInfo info = m_RequestQueue.dequeue(); + info.m_Timeout = new QTimer(this); + info.m_Timeout->setInterval(60000); + + QJsonObject postObject; + QJsonDocument postData(postObject); + + QString url; + if (!info.m_Reroute) { + bool hasParams = false; + switch (info.m_Type) { + case NXMRequestInfo::TYPE_DESCRIPTION: { + url = QString("%1/games/%2/mods/%3").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); + } break; + case NXMRequestInfo::TYPE_FILES: { + url = QString("%1/games/%2/mods/%3/files").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); + } break; + case NXMRequestInfo::TYPE_GETUPDATES: { + url = QString("%1/games/%2/mods/%3/files").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); + } break; + case NXMRequestInfo::TYPE_FILEINFO: { + url = QString("%1/games/%2/mods/%3/files/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); + } break; + case NXMRequestInfo::TYPE_DOWNLOADURL: { + url = QString("%1/games/%2/mods/%3/files/%4/download_link").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); + } break; + case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { + QString endorse = info.m_Endorse ? "endorse" : "abstain"; + url = QString("%1/games/%2/mods/%3/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(endorse); + postObject.insert("Version", info.m_ModVersion); + postData.setObject(postObject); + } break; + } + } else { + url = info.m_URL; + } + QNetworkRequest request(url); + request.setRawHeader("apikey", m_AccessManager->apiKey().toUtf8()); + request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m_AccessManager->userAgent(info.m_SubModule)); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + request.setRawHeader("Protocol-Version", "0.5.5"); + request.setRawHeader("Application-Version", QApplication::applicationVersion().toUtf8()); + + if (postData.object().isEmpty()) + info.m_Reply = m_AccessManager->get(request); + else + info.m_Reply = m_AccessManager->post(request, postData.toJson()); + + connect(info.m_Reply, SIGNAL(finished()), this, SLOT(requestFinished())); + connect(info.m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); + connect(info.m_Timeout, SIGNAL(timeout()), this, SLOT(requestTimeout())); + info.m_Timeout->start(); + m_ActiveRequest.push_back(info); +} + + +void NexusInterface::downloadRequestedNXM(const QString &url) +{ + emit requestNXMDownload(url); +} + +void NexusInterface::requestFinished(std::list::iterator iter) +{ + QNetworkReply *reply = iter->m_Reply; + + if (reply->error() != QNetworkReply::NoError) { + qWarning("request failed: %s", reply->errorString().toUtf8().constData()); + emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), reply->errorString()); + } else { + int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (statusCode == 301) { + // redirect request, return request to queue + iter->m_URL = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toString(); + iter->m_Reroute = true; + m_RequestQueue.enqueue(*iter); + //nextRequest(); + return; + } + QByteArray data = reply->readAll(); + if (data.isNull() || data.isEmpty() || (strcmp(data.constData(), "null") == 0)) { + QString nexusError(reply->rawHeader("NexusErrorInfo")); + if (nexusError.length() == 0) { + nexusError = tr("empty response"); + } + qDebug("nexus error: %s", qUtf8Printable(nexusError)); + emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), nexusError); + } else { + bool ok; + QVariant result = QtJson::parse(data, ok); + if (result.isValid() && ok) { + switch (iter->m_Type) { + case NXMRequestInfo::TYPE_DESCRIPTION: { + emit nxmDescriptionAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); + } break; + case NXMRequestInfo::TYPE_FILES: { + emit nxmFilesAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); + } break; + case NXMRequestInfo::TYPE_FILEINFO: { + emit nxmFileInfoAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID); + } break; + case NXMRequestInfo::TYPE_DOWNLOADURL: { + emit nxmDownloadURLsAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID); + } break; + case NXMRequestInfo::TYPE_GETUPDATES: { + emit nxmUpdatesAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); + } break; + case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { + emit nxmEndorsementToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); + } break; + } + } else { + emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), tr("invalid response")); + } + } + } +} + + +void NexusInterface::requestFinished() +{ + QNetworkReply *reply = static_cast(sender()); + for (std::list::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) { + if (iter->m_Reply == reply) { + iter->m_Timeout->stop(); + iter->m_Timeout->deleteLater(); + requestFinished(iter); + iter->m_Reply->deleteLater(); + m_ActiveRequest.erase(iter); + nextRequest(); + return; + } + } +} + + +void NexusInterface::requestError(QNetworkReply::NetworkError) +{ + QNetworkReply *reply = qobject_cast(sender()); + if (reply == nullptr) { + qWarning("invalid sender type"); + return; + } + + qCritical("request (%s) error: %s (%d)", + qUtf8Printable(reply->url().toString()), + qUtf8Printable(reply->errorString()), + reply->error()); +} + + +void NexusInterface::requestTimeout() +{ + QTimer *timer = qobject_cast(sender()); + if (timer == nullptr) { + qWarning("invalid sender type"); + return; + } + for (std::list::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) { + if (iter->m_Timeout == timer) { + // this abort causes a "request failed" which cleans up the rest + iter->m_Reply->abort(); + return; + } + } +} + +namespace { + QString get_management_url(MOBase::IPluginGame const *game) + { + return "https://api.nexusmods.com/v1"; + } +} + +NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID + , NexusInterface::NXMRequestInfo::Type type + , QVariant userData + , const QString &subModule + , MOBase::IPluginGame const *game + ) + : m_ModID(modID) + , m_ModVersion("0") + , m_FileID(0) + , m_Reply(nullptr) + , m_Type(type) + , m_UserData(userData) + , m_Timeout(nullptr) + , m_Reroute(false) + , m_ID(s_NextID.fetchAndAddAcquire(1)) + , m_URL(get_management_url(game)) + , m_SubModule(subModule) + , m_NexusGameID(game->nexusGameID()) + , m_GameName(game->gameNexusName()) + , m_Endorse(false) +{} + +NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID + , QString modVersion + , NexusInterface::NXMRequestInfo::Type type + , QVariant userData + , const QString &subModule + , MOBase::IPluginGame const *game +) + : m_ModID(modID) + , m_ModVersion(modVersion) + , m_FileID(0) + , m_Reply(nullptr) + , m_Type(type) + , m_UserData(userData) + , m_Timeout(nullptr) + , m_Reroute(false) + , m_ID(s_NextID.fetchAndAddAcquire(1)) + , m_URL(get_management_url(game)) + , m_SubModule(subModule) + , m_NexusGameID(game->nexusGameID()) + , m_GameName(game->gameNexusName()) + , m_Endorse(false) +{} + +NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID + , int fileID + , NexusInterface::NXMRequestInfo::Type type + , QVariant userData + , const QString &subModule + , MOBase::IPluginGame const *game +) + : m_ModID(modID) + , m_ModVersion("0") + , m_FileID(fileID) + , m_Reply(nullptr) + , m_Type(type) + , m_UserData(userData) + , m_Timeout(nullptr) + , m_Reroute(false) + , m_ID(s_NextID.fetchAndAddAcquire(1)) + , m_URL(get_management_url(game)) + , m_SubModule(subModule) + , m_NexusGameID(game->nexusGameID()) + , m_GameName(game->gameNexusName()) + , m_Endorse(false) +{} diff --git a/src/nexusinterface.h b/src/nexusinterface.h index 7b707711..56cdef47 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -1,424 +1,423 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#ifndef NEXUSINTERFACE_H -#define NEXUSINTERFACE_H - -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include - -namespace MOBase { class IPluginGame; } - -class NexusInterface; -class NXMAccessManager; - -/** - * @brief convenience class to make nxm requests easier - * usually, all objects that started a nxm request will be signaled if one finished. - * Therefore, the objects need to store the id of the requests they started and then filter - * the result. - * NexusBridge does this automatically. Users connect to the signals of NexusBridge they intend - * to handle and only receive the signals the caused - **/ -class NexusBridge : public MOBase::IModRepositoryBridge -{ - - Q_OBJECT - -public: - - NexusBridge(PluginContainer *pluginContainer, const QString &subModule = ""); - - /** - * @brief request description for a mod - * - * @param modID id of the mod caller is interested in - * @param userData user data to be returned with the result - * @param url the url to request from - **/ - virtual void requestDescription(QString gameName, int modID, QVariant userData); - - /** - * @brief request a list of the files belonging to a mod - * - * @param modID id of the mod caller is interested in - * @param userData user data to be returned with the result - **/ - virtual void requestFiles(QString gameName, int modID, QVariant userData); - - /** - * @brief request info about a single file of a mod - * - * @param modID id of the mod caller is interested in - * @param fileID id of the file the caller is interested in - * @param userData user data to be returned with the result - **/ - virtual void requestFileInfo(QString gameName, int modID, int fileID, QVariant userData); - - /** - * @brief request the download url of a file - * - * @param modID id of the mod caller is interested in - * @param fileID id of the file the caller is interested in - * @param userData user data to be returned with the result - **/ - virtual void requestDownloadURL(QString gameName, int modID, int fileID, QVariant userData); - - /** - * @brief requestToggleEndorsement - * @param modID id of the mod caller is interested in - * @param userData user data to be returned with the result - */ - virtual void requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData); - -public slots: - - void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); - void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); - void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, const QString &errorMessage); - -private: - - NexusInterface *m_Interface; - QString m_SubModule; - std::set m_RequestIDs; - -}; - - -/** - * @brief Makes asynchronous requests to the nexus API - * - * This class can be used to make asynchronous requests to the Nexus API. - * Currently, responses are sent to all receivers that have sent a request of the relevant type, so the - * recipient has to filter the response by the id returned when making the request - **/ -class NexusInterface : public QObject -{ - Q_OBJECT - -public: - - ~NexusInterface(); - - static NexusInterface *instance(PluginContainer *pluginContainer); - - /** - * @return the access manager object used to connect to nexus - **/ - NXMAccessManager *getAccessManager(); - - /** - * @brief cleanup this interface. this is destructive, afterwards it can't be used again - */ - void cleanup(); - - /** - * @brief clear webcache and cookies associated with this access manager - */ - void clearCache(); - - /** - * @brief request description for a mod - * - * @param modID id of the mod caller is interested in (assumed to be for the current game) - * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable) - * @param userData user data to be returned with the result - * @return int an id to identify the request - **/ - int requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule) - { - return requestDescription(gameName, modID, receiver, userData, subModule, getGame(gameName)); - } - - /** - * @brief request description for a mod - * - * @param modID id of the mod caller is interested in - * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable) - * @param userData user data to be returned with the result - * @param game Game with which the mod is associated - * @return int an id to identify the request - **/ - int requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule, - MOBase::IPluginGame const *game); - - /** - * @brief request nexus descriptions for multiple mods at once - * @param modIDs a list of ids of mods the caller is interested in - * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable) - * @param userData user data to be returned with the result - * @param game the game with which the mods are associated - * @return int an id to identify the request - */ - int requestUpdates(const std::vector &modIDs, QObject *receiver, QVariant userData, QString gameName, const QString &subModule); - - /** - * @brief request a list of the files belonging to a mod - * - * @param modID id of the mod caller is interested in (assumed to be for the current game) - * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) - * @param userData user data to be returned with the result - * @return int an id to identify the request - **/ - int requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule) - { - return requestFiles(gameName, modID, receiver, userData, subModule, getGame(gameName)); - } - - - /** - * @brief request a list of the files belonging to a mod - * - * @param modID id of the mod caller is interested in - * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) - * @param userData user data to be returned with the result - * @param game the game with which the mods are associated - * @return int an id to identify the request - **/ - int requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule, - MOBase::IPluginGame const *game); - - /** - * @brief request info about a single file of a mod - * - * @param game name of the game short name to request the download from - * @param modID id of the mod caller is interested in (assumed to be for the current game) - * @param fileID id of the file the caller is interested in - * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) - * @param userData user data to be returned with the result - * @return int an id to identify the request - **/ - int requestFileInfo(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule); - - /** - * @brief request the download url of a file - * - * @param modID id of the mod caller is interested in (assumed to be for the current game) - * @param fileID id of the file the caller is interested in - * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) - * @param userData user data to be returned with the result - * @return int an id to identify the request - **/ - int requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule) - { - return requestDownloadURL(gameName, modID, fileID, receiver, userData, subModule, getGame(gameName)); - } - - /** - * @brief request the download url of a file - * - * @param modID id of the mod caller is interested in - * @param fileID id of the file the caller is interested in - * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) - * @param userData user data to be returned with the result - * @param game the game with which the mods are associated - * @return int an id to identify the request - **/ - int requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); - - /** - * @brief toggle endorsement state of the mod - * @param modID id of the mod (assumed to be for the current game) - * @param endorse true if the mod should be endorsed, false for un-endorse - * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) - * @param userData user data to be returned with the result - * @return int an id to identify the request - */ - int requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule) - { - return requestToggleEndorsement(gameName, modID, modVersion, endorse, receiver, userData, subModule, getGame(gameName)); - } - - /** - * @brief toggle endorsement state of the mod - * @param modID id of the mod - * @param endorse true if the mod should be endorsed, false for un-endorse - * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) - * @param userData user data to be returned with the result - * @param game the game with which the mods are associated - * @return int an id to identify the request - */ - int requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule, - MOBase::IPluginGame const *game); - - /** - * @param directory the directory to store cache files - **/ - void setCacheDirectory(const QString &directory); - - /** - * MO has to send a "Nexus Client Vx.y.z" as part of the user agent to be allowed to use the API - * @param nmmVersion the version of nmm to impersonate - **/ - void setNMMVersion(const QString &nmmVersion); - - /** - * @brief called when the log-in completes. This was, requests waiting for the log-in can be run - */ - void loginCompleted(); - - std::vector> getGameChoices(const MOBase::IPluginGame *game); - -public: - - /** - * @brief guess the mod id from a filename as delivered by Nexus - * @param fileName name of the file - * @return the guessed mod id - * @note this currently doesn't fit well with the remaining interface but this is the best place for the function - */ - static void interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query); - - /** - * @brief get the currently managed game - */ - MOBase::IPluginGame const *managedGame() const; - - /** - * @brief see if the passed URL is related to the current game - * - * Arguably, this should optionally take a gameplugin pointer - */ - bool isURLGameRelated(QUrl const &url) const; - - /** - * @brief Get the nexus page for the current game - * - * Arguably, this should optionally take a gameplugin pointer - */ - QString getGameURL(QString gameName) const; - - /** - * @brief Get the URL for the mod web page - * @param modID - */ - QString getModURL(int modID, QString gameName) const; - - /** - * @brief Checks if the specified URL might correspond to a nexus mod - * @param modID - * @param url - * @return - */ - bool isModURL(int modID, QString const &url) const; - - void setPluginContainer(PluginContainer *pluginContainer); - -signals: - - void requestNXMDownload(const QString &url); - - void needLogin(); - - void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - void nxmUpdatesAvailable(const std::vector &modIDs, QVariant userData, QVariant resultData, int requestID); - void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); - void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); - void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); - void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, const QString &errorString); - -private slots: - - void requestFinished(); - void requestError(QNetworkReply::NetworkError error); - void requestTimeout(); - - void downloadRequestedNXM(const QString &url); - - void fakeFiles(); - -private: - - struct NXMRequestInfo { - int m_ModID; - QString m_ModVersion; - std::vector m_ModIDList; - int m_FileID; - QNetworkReply *m_Reply; - enum Type { - TYPE_DESCRIPTION, - TYPE_FILES, - TYPE_FILEINFO, - TYPE_DOWNLOADURL, - TYPE_TOGGLEENDORSEMENT, - TYPE_GETUPDATES - } m_Type; - QVariant m_UserData; - QTimer *m_Timeout; - QString m_URL; - QString m_SubModule; - QString m_GameName; - int m_NexusGameID; - bool m_Reroute; - int m_ID; - int m_Endorse; - - NXMRequestInfo(int modID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); - NXMRequestInfo(int modID, QString modVersion, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); - NXMRequestInfo(std::vector modIDList, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); - NXMRequestInfo(int modID, int fileID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); - - private: - static QAtomicInt s_NextID; - }; - - static const int MAX_ACTIVE_DOWNLOADS = 2; - -private: - - NexusInterface(PluginContainer *pluginContainer); - void nextRequest(); - void requestFinished(std::list::iterator iter); - bool requiresLogin(const NXMRequestInfo &info); - MOBase::IPluginGame *getGame(QString gameName) const; - QString getOldModsURL(QString gameName) const; - -private: - - QNetworkDiskCache *m_DiskCache; - - NXMAccessManager *m_AccessManager; - - std::list m_ActiveRequest; - QQueue m_RequestQueue; - - MOBase::VersionInfo m_MOVersion; - QString m_NMMVersion; - - PluginContainer *m_PluginContainer; - -}; - -#endif // NEXUSINTERFACE_H +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef NEXUSINTERFACE_H +#define NEXUSINTERFACE_H + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace MOBase { class IPluginGame; } + +class NexusInterface; +class NXMAccessManager; + +/** + * @brief convenience class to make nxm requests easier + * usually, all objects that started a nxm request will be signaled if one finished. + * Therefore, the objects need to store the id of the requests they started and then filter + * the result. + * NexusBridge does this automatically. Users connect to the signals of NexusBridge they intend + * to handle and only receive the signals the caused + **/ +class NexusBridge : public MOBase::IModRepositoryBridge +{ + + Q_OBJECT + +public: + + NexusBridge(PluginContainer *pluginContainer, const QString &subModule = ""); + + /** + * @brief request description for a mod + * + * @param modID id of the mod caller is interested in + * @param userData user data to be returned with the result + * @param url the url to request from + **/ + virtual void requestDescription(QString gameName, int modID, QVariant userData); + + /** + * @brief request a list of the files belonging to a mod + * + * @param modID id of the mod caller is interested in + * @param userData user data to be returned with the result + **/ + virtual void requestFiles(QString gameName, int modID, QVariant userData); + + /** + * @brief request info about a single file of a mod + * + * @param modID id of the mod caller is interested in + * @param fileID id of the file the caller is interested in + * @param userData user data to be returned with the result + **/ + virtual void requestFileInfo(QString gameName, int modID, int fileID, QVariant userData); + + /** + * @brief request the download url of a file + * + * @param modID id of the mod caller is interested in + * @param fileID id of the file the caller is interested in + * @param userData user data to be returned with the result + **/ + virtual void requestDownloadURL(QString gameName, int modID, int fileID, QVariant userData); + + /** + * @brief requestToggleEndorsement + * @param modID id of the mod caller is interested in + * @param userData user data to be returned with the result + */ + virtual void requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData); + +public slots: + + void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorMessage); + +private: + + NexusInterface *m_Interface; + QString m_SubModule; + std::set m_RequestIDs; + +}; + + +/** + * @brief Makes asynchronous requests to the nexus API + * + * This class can be used to make asynchronous requests to the Nexus API. + * Currently, responses are sent to all receivers that have sent a request of the relevant type, so the + * recipient has to filter the response by the id returned when making the request + **/ +class NexusInterface : public QObject +{ + Q_OBJECT + +public: + + ~NexusInterface(); + + static NexusInterface *instance(PluginContainer *pluginContainer); + + /** + * @return the access manager object used to connect to nexus + **/ + NXMAccessManager *getAccessManager(); + + /** + * @brief cleanup this interface. this is destructive, afterwards it can't be used again + */ + void cleanup(); + + /** + * @brief clear webcache and cookies associated with this access manager + */ + void clearCache(); + + /** + * @brief request description for a mod + * + * @param modID id of the mod caller is interested in (assumed to be for the current game) + * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable) + * @param userData user data to be returned with the result + * @return int an id to identify the request + **/ + int requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule) + { + return requestDescription(gameName, modID, receiver, userData, subModule, getGame(gameName)); + } + + /** + * @brief request description for a mod + * + * @param modID id of the mod caller is interested in + * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable) + * @param userData user data to be returned with the result + * @param game Game with which the mod is associated + * @return int an id to identify the request + **/ + int requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule, + MOBase::IPluginGame const *game); + + /** + * @brief request nexus descriptions for multiple mods at once + * @param modIDs a list of ids of mods the caller is interested in + * @param receiver the object to receive the result asynchronously via a signal (nxmDescriptionAvailable) + * @param userData user data to be returned with the result + * @param game the game with which the mods are associated + * @return int an id to identify the request + */ + int requestUpdates(const int &modID, QObject *receiver, QVariant userData, QString gameName, const QString &subModule); + + /** + * @brief request a list of the files belonging to a mod + * + * @param modID id of the mod caller is interested in (assumed to be for the current game) + * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @return int an id to identify the request + **/ + int requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule) + { + return requestFiles(gameName, modID, receiver, userData, subModule, getGame(gameName)); + } + + + /** + * @brief request a list of the files belonging to a mod + * + * @param modID id of the mod caller is interested in + * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @param game the game with which the mods are associated + * @return int an id to identify the request + **/ + int requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule, + MOBase::IPluginGame const *game); + + /** + * @brief request info about a single file of a mod + * + * @param game name of the game short name to request the download from + * @param modID id of the mod caller is interested in (assumed to be for the current game) + * @param fileID id of the file the caller is interested in + * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @return int an id to identify the request + **/ + int requestFileInfo(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule); + + /** + * @brief request the download url of a file + * + * @param modID id of the mod caller is interested in (assumed to be for the current game) + * @param fileID id of the file the caller is interested in + * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @return int an id to identify the request + **/ + int requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule) + { + return requestDownloadURL(gameName, modID, fileID, receiver, userData, subModule, getGame(gameName)); + } + + /** + * @brief request the download url of a file + * + * @param modID id of the mod caller is interested in + * @param fileID id of the file the caller is interested in + * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @param game the game with which the mods are associated + * @return int an id to identify the request + **/ + int requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + + /** + * @brief toggle endorsement state of the mod + * @param modID id of the mod (assumed to be for the current game) + * @param endorse true if the mod should be endorsed, false for un-endorse + * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @return int an id to identify the request + */ + int requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule) + { + return requestToggleEndorsement(gameName, modID, modVersion, endorse, receiver, userData, subModule, getGame(gameName)); + } + + /** + * @brief toggle endorsement state of the mod + * @param modID id of the mod + * @param endorse true if the mod should be endorsed, false for un-endorse + * @param receiver the object to receive the result asynchronously via a signal (nxmFilesAvailable) + * @param userData user data to be returned with the result + * @param game the game with which the mods are associated + * @return int an id to identify the request + */ + int requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule, + MOBase::IPluginGame const *game); + + /** + * @param directory the directory to store cache files + **/ + void setCacheDirectory(const QString &directory); + + /** + * MO has to send a "Nexus Client Vx.y.z" as part of the user agent to be allowed to use the API + * @param nmmVersion the version of nmm to impersonate + **/ + void setNMMVersion(const QString &nmmVersion); + + /** + * @brief called when the log-in completes. This was, requests waiting for the log-in can be run + */ + void loginCompleted(); + + std::vector> getGameChoices(const MOBase::IPluginGame *game); + +public: + + /** + * @brief guess the mod id from a filename as delivered by Nexus + * @param fileName name of the file + * @return the guessed mod id + * @note this currently doesn't fit well with the remaining interface but this is the best place for the function + */ + static void interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query); + + /** + * @brief get the currently managed game + */ + MOBase::IPluginGame const *managedGame() const; + + /** + * @brief see if the passed URL is related to the current game + * + * Arguably, this should optionally take a gameplugin pointer + */ + bool isURLGameRelated(QUrl const &url) const; + + /** + * @brief Get the nexus page for the current game + * + * Arguably, this should optionally take a gameplugin pointer + */ + QString getGameURL(QString gameName) const; + + /** + * @brief Get the URL for the mod web page + * @param modID + */ + QString getModURL(int modID, QString gameName) const; + + /** + * @brief Checks if the specified URL might correspond to a nexus mod + * @param modID + * @param url + * @return + */ + bool isModURL(int modID, QString const &url) const; + + void setPluginContainer(PluginContainer *pluginContainer); + +signals: + + void requestNXMDownload(const QString &url); + + void needLogin(); + + void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); + +private slots: + + void requestFinished(); + void requestError(QNetworkReply::NetworkError error); + void requestTimeout(); + + void downloadRequestedNXM(const QString &url); + + void fakeFiles(); + +private: + + struct NXMRequestInfo { + int m_ModID; + QString m_ModVersion; + std::vector m_ModIDList; + int m_FileID; + QNetworkReply *m_Reply; + enum Type { + TYPE_DESCRIPTION, + TYPE_FILES, + TYPE_FILEINFO, + TYPE_DOWNLOADURL, + TYPE_TOGGLEENDORSEMENT, + TYPE_GETUPDATES + } m_Type; + QVariant m_UserData; + QTimer *m_Timeout; + QString m_URL; + QString m_SubModule; + QString m_GameName; + int m_NexusGameID; + bool m_Reroute; + int m_ID; + int m_Endorse; + + NXMRequestInfo(int modID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + NXMRequestInfo(int modID, QString modVersion, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + NXMRequestInfo(int modID, int fileID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + + private: + static QAtomicInt s_NextID; + }; + + static const int MAX_ACTIVE_DOWNLOADS = 2; + +private: + + NexusInterface(PluginContainer *pluginContainer); + void nextRequest(); + void requestFinished(std::list::iterator iter); + bool requiresLogin(const NXMRequestInfo &info); + MOBase::IPluginGame *getGame(QString gameName) const; + QString getOldModsURL(QString gameName) const; + +private: + + QNetworkDiskCache *m_DiskCache; + + NXMAccessManager *m_AccessManager; + + std::list m_ActiveRequest; + QQueue m_RequestQueue; + + MOBase::VersionInfo m_MOVersion; + QString m_NMMVersion; + + PluginContainer *m_PluginContainer; + +}; + +#endif // NEXUSINTERFACE_H diff --git a/src/organizer_en.ts b/src/organizer_en.ts index bcdbd069..0d63c34c 100644 --- a/src/organizer_en.ts +++ b/src/organizer_en.ts @@ -571,301 +571,301 @@ p, li { white-space: pre-wrap; } - + Memory allocation error (in refreshing directory). - + failed to download %1: could not open output file: %2 - + Download again? - + A file with the same name "%1" has already been downloaded. Do you want to download it again? The new file will receive a different name. - + Wrong Game - + The download link is for a mod for "%1" but this instance of MO has been set up for "%2". - - + + Already Started - + A download for this mod file has already been queued. - + There is already a download started for this file (mod: %1, file: %2). - - + + remove: invalid download index %1 - + failed to delete %1 - + failed to delete meta file for %1 - + restore: invalid download index: %1 - + cancel: invalid download index %1 - + pause: invalid download index %1 - + resume: invalid download index %1 - + resume (int): invalid download index %1 - + No known download urls. Sorry, this download can't be resumed. - + query: invalid download index %1 - + Please enter the nexus mod id - + Mod ID: - + Please select the source game code for %1 - + VisitNexus: invalid download index %1 - + Nexus ID for this Mod is unknown - + OpenFile: invalid download index %1 - + OpenFileInDownloadsFolder: invalid download index %1 - + get pending: invalid download index %1 - + get path: invalid download index %1 - + Main - + Update - + Optional - + Old - + Misc - + Unknown - + display name: invalid download index %1 - + file name: invalid download index %1 - + file time: invalid download index %1 - + file size: invalid download index %1 - + progress: invalid download index %1 - + state: invalid download index %1 - + infocomplete: invalid download index %1 - - + + mod id: invalid download index %1 - + ishidden: invalid download index %1 - + file info: invalid download index %1 - + mark installed: invalid download index %1 - + mark uninstalled: invalid download index %1 - + Memory allocation error (in processing progress event). - + Memory allocation error (in processing downloaded data). - + Information updated - - + + No matching file found on Nexus! Maybe this file is no longer available or it was renamed? - + No file on Nexus matches the selected file by name. Please manually choose the correct one. - + No download server available. Please try again later. - + Failed to request file info from nexus: %1 - + Warning: Content type is: %1 - + Download header content length: %1 downloaded file size: %2 - + Download failed: %1 (%2) - + We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers. - + failed to re-open %1 - + Unable to write download to drive (return %1). Check the drive's available storage. @@ -2431,7 +2431,7 @@ Please enter a name: - + Are you sure? @@ -2768,13 +2768,13 @@ You can also use online editors and converters instead. - + Enable selected - + Disable selected @@ -2835,13 +2835,13 @@ You can also use online editors and converters instead. - + Exception: - + Unknown exception @@ -2979,7 +2979,7 @@ Click OK to restart MO now. - + Set Priority @@ -3044,206 +3044,206 @@ Click OK to restart MO now. - + Thank you! - + Thank you for your endorsement! - + Okay. - + This mod will not be endorsed and will no longer ask you to endorse. - + Request to Nexus failed: %1 - - + + failed to read %1: %2 - - + + Error - + failed to extract %1 (errorcode %2) - + Extract BSA - + This archive contains invalid hashes. Some files may be broken. - + Extract... - + This will restart MO, continue? - + Edit Categories... - + Deselect filter - + Remove - + Enable all - + Disable all - + Unlock load order - + Lock load order - + Open Origin in Explorer - + Open Origin Info... - + depends on missing "%1" - + incompatible with "%1" - + Please wait while LOOT is running - + loot failed. Exit code was: %1 - + failed to start loot - + failed to run loot: %1 - + Errors occured - + Backup of load order created - + Choose backup to restore - + No Backups - + There are no backups to restore - - + + Restore failed - - + + Failed to restore the backup. Errorcode: %1 - + Backup of modlist created - + A file with the same name has already been downloaded. What would you like to do? - + Overwrite - + Rename new file - + Ignore file - + Set the priority of the selected mods @@ -3768,7 +3768,7 @@ p, li { white-space: pre-wrap; } - Misc + Deleted @@ -3990,12 +3990,12 @@ p, li { white-space: pre-wrap; } - + %1 contains no esp/esm/esl and no asset (textures, meshes, interface, ...) directory - + Categories: <br> @@ -4350,7 +4350,7 @@ p, li { white-space: pre-wrap; } - timeout + There was a timeout during the request @@ -4370,17 +4370,17 @@ p, li { white-space: pre-wrap; } NexusInterface - + Failed to guess mod id for "%1", please pick the correct one - + empty response - + invalid response @@ -5617,7 +5617,7 @@ If the folder was still in use, restart MO and try again. - + Mod Organizer @@ -5694,12 +5694,12 @@ If the folder was still in use, restart MO and try again. - + Script Extender - + Proxy DLL @@ -5921,28 +5921,28 @@ Select Show Details option to see the full change-log. - - + + Error - + Failed to retrieve a Nexus API key! Please try again.A browser window should open asking you to authorize. - + Failed to create "%1", you may not have the necessary permission. path remains unchanged. - + Restart Mod Organizer? - + In order to reset the window geometries, MO must be restarted. Restart it now? @@ -6215,137 +6215,147 @@ p, li { white-space: pre-wrap; } - - Remove cache and cookies. Forces a new login. + + Clear the stored Nexus API key and force reauthorization. + + + + + Revoke Nexus Authorization - + + Remove cache and cookies. + + + + Clear Cache - + Disable automatic internet features - + Disable automatic internet features. This does not affect features that are explicitly invoked by the user (like checking mods for updates, endorsing, opening the web browser) - + Offline Mode - + Use a proxy for network connections. - + Use a proxy for network connections. This uses the system-wide settings which can be configured in Internet Explorer. Please note that MO will start up a few seconds slower on some systems when using a proxy. - + Use HTTP Proxy (Uses System Settings) - + Endorsement Integration - + Associate with "Download with manager" links - + Known Servers (updated on download) - + Preferred Servers (Drag & Drop) - + Steam - + Username - + Password - + If you save your steam user ID and password here, they will be used when logging into steam. Note, however, your password will be stored unencrypted, so make sure your computer is secure. - + Plugins - + Author: - + Version: - + Description: - + Key - + Value - + Blacklisted Plugins (use <del> to remove): - + Workarounds - + Steam App ID - + The Steam AppID for your game - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -6361,17 +6371,17 @@ p, li { white-space: pre-wrap; } - + Load Mechanism - + Select loading mechanism. See help for details. - + Mod Organizer needs a dll to be injected into the game so all mods are visible to it. There are several means to do this: *Mod Organizer* (default) In this mode the Mod Organizer itself injects the dll. The disadvantage is that you always have to start the game through MO or a link created by it. @@ -6382,17 +6392,17 @@ If you use the Steam version of Oblivion the default will NOT work. In this case - + NMM Version - + The Version of Nexus Mod Manager to impersonate. - + Mod Organizer uses an API provided by the Nexus to provide features like checking for updates and downloading files. Unfortunately this API has not been made available officially to third party tools like MO so we have to impersonate the Nexus Mod Manager to be allowed in. On top of this Nexus has used the client identification to lock out outdated versions of NMM to force users to update. This means that MO also needs to impersonate the new version of NMM even if MO doesn't need an update. Therefore you can configure the version to identify as here. Please note that MO does identify itself as MO to the webserver, it's not lying about what it is. It is merely adding a "compatible" NMM version to the user agent. @@ -6401,28 +6411,28 @@ tl;dr-version: If Nexus-features don't work, insert the current version num - + Enforces that inactive ESPs and ESMs are never loaded. - + It seems that the Games occasionally load ESP or ESM files even if they haven't been activated as plugins. I don't yet know what the circumstances are, but user reports imply it is in some cases unwanted. If this is checked, ESPs and ESMs not checked in the List are invisible to the game and can not be loaded. - + Hide inactive ESPs/ESMs - + Disable this to no longer display mods installed outside MO in the mod list (left pane). Assets from those mods will then be treated as having lowest mod priority together with the original game content. - + By default Mod Organizer will display esp+bsa bundles installed with foreign tools as mods (left pane). This allows you to control their priority in relation to other mods. This is particularly useful if you also use Steam Workshop to install mods. However, if you installed loose file mods outside MO which conflict with BSAs also installed outside MO those conflicts can't be resolved correctly. @@ -6430,66 +6440,66 @@ If you disable this feature, MO will only display official DLCs this way. Please - + Display mods installed outside MO - + If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) - + If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) Uncheck this if you want to use Mod Organizer with total conversions (like Nehrim) but be aware that the game will crash if required files are not enabled. - + Force-enable game files - - + + Disable this to prevent the GUI from being locked when running an executable. This may result in abnormal behavior. - + Lock GUI when running executable - + Enable parsing of Archives. Has negative effects on performance. - + <html><head/><body><p>By default, MO will parse archive files (BSA, BA2) to calculate conflicts between the contents of the archive files and other loose files. This process has a noticeable cost in performance.</p><p>This feature should not be confused with the archive management feature offered by MO1. MO2 will only show conflicts with archives and will NOT load them into the game or program.</p><p>If you disable this feature, MO will only display conflicts between loose files.</p></body></html> - + Enable parsing of Archives - - + + For Skyrim, this can be used instead of Archive Invalidation. It should make AI redundant for all Profiles. For the other games this is not a sufficient replacement for AI! - + Back-date BSAs - + Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended @@ -6498,48 +6508,48 @@ programs you are intentionally running. - + Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended programs may affect the execution of these programs or the programs you are intentionally running. - + Configure Executables Blacklist - - + + Resets the window geometries for all windows. This can be useful if a window becomes too small or too large, if a column becomes too thin or too wide, and in similar situations. - + Reset Window Geometries - + These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here. - + Diagnostics - + Max Dumps To Keep - + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. - + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. Set "Crash Dumps" above to None to disable crash dump collection. @@ -6547,12 +6557,12 @@ programs you are intentionally running. - + Hint: right click link and copy link location - + Logs and crash dumps are stored under your current instance in the <a href="LOGS_FULL_PATH">LOGS_DIR</a> and <a href="DUMPS_FULL_PATH">DUMPS_DIR</a> folders. @@ -6562,17 +6572,17 @@ programs you are intentionally running. - + Crash Dumps - + Decides which type of crash dumps are collected when injected processes crash. - + Decides which type of crash dumps are collected when injected processes crash. "None" Disables the generation of crash dumps by MO. @@ -6583,37 +6593,37 @@ programs you are intentionally running. - + None - + Mini (recommended) - + Data - + Full - + Log Level - + Decides the amount of data printed to "ModOrganizer.log" - + Decides the amount of data printed to "ModOrganizer.log". "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regluar use. On the "Error" level the log file usually remains empty. @@ -6621,22 +6631,22 @@ programs you are intentionally running. - + Debug - + Info (recommended) - + Warning - + Error -- cgit v1.3.1 From f07f6b8904f69a92cd24e3c52eaf1fc6db8e60dd Mon Sep 17 00:00:00 2001 From: Silarn Date: Tue, 29 Jan 2019 20:10:53 -0600 Subject: Update Process Improvements * 5 minute batch auto-update of up to 10 mods - Still able to force an update of all 'unchecked' mods - Prioritizes mods with oldest update 'age' * Implemented 1-hour wait between update checks per mod * Fixed issues with update progress display * Only enable update filter automatically if 'force update' * Improved display of version update status in mod list - Italic text when ready to perform update check - Tooltip indicates when next update is available * Fixed remaining issues with update fallback check * Only trigger one update API request for duplicate sources --- src/mainwindow.cpp | 80 +- src/mainwindow.h | 3 + src/modinfo.cpp | 49 +- src/modinfo.h | 1433 ++++++++++++------------ src/modinfodialog.cpp | 2 +- src/modinfoforeign.h | 3 + src/modinfooverwrite.h | 3 + src/modinforegular.cpp | 42 +- src/modinforegular.h | 18 +- src/modinfoseparator.h | 4 +- src/modlist.cpp | 2808 ++++++++++++++++++++++++------------------------ 11 files changed, 2288 insertions(+), 2157 deletions(-) (limited to 'src/modinfo.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 094a44af..b57c62f8 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -400,7 +400,7 @@ MainWindow::MainWindow(QSettings &initSettings connect(NexusInterface::instance(&pluginContainer), SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), this, SLOT(nxmDownloadURLs(QString,int,int,QVariant,QVariant,int))); connect(NexusInterface::instance(&pluginContainer), SIGNAL(needLogin()), &m_OrganizerCore, SLOT(nexusApi())); connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(validateFailed(QString)), this, SLOT(validationFailed(QString))); - connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool)), + connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool, std::tuple)), this, SLOT(updateWindowTitle(const QString&, bool))); connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, bool, std::tuple)), NexusInterface::instance(&m_PluginContainer), SLOT(setRateMax(const QString&, bool, std::tuple))); @@ -439,6 +439,10 @@ MainWindow::MainWindow(QSettings &initSettings connect(&m_SaveMetaTimer, SIGNAL(timeout()), this, SLOT(saveModMetas())); m_SaveMetaTimer.start(5000); + m_ModUpdateTimer.setSingleShot(false); + connect(&m_ModUpdateTimer, SIGNAL(timeout()), this, SLOT(modUpdateCheck())); + m_ModUpdateTimer.start(300 * 1000); + setCategoryListVisible(initSettings.value("categorylist_visible", true).toBool()); FileDialogMemory::restore(initSettings); @@ -491,6 +495,8 @@ MainWindow::MainWindow(QSettings &initSettings updatePluginCount(); updateModCount(); + + modUpdateCheck(); } @@ -3996,6 +4002,14 @@ void MainWindow::checkModsForUpdates() m_ModsToUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this); } } + + m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)); + for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) { + if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { + ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i)); + break; + } + } } void MainWindow::changeVersioningScheme() { @@ -4399,7 +4413,7 @@ void MainWindow::initModListContextMenu(QMenu *menu) menu->addAction(tr("Enable all visible"), this, SLOT(enableVisibleMods())); menu->addAction(tr("Disable all visible"), this, SLOT(disableVisibleMods())); - menu->addAction(tr("Check all for update"), this, SLOT(checkModsForUpdates())); + menu->addAction(tr("Force update check"), this, SLOT(checkModsForUpdates())); menu->addAction(tr("Refresh"), &m_OrganizerCore, SLOT(profileRefresh())); menu->addAction(tr("Export to csv..."), this, SLOT(exportModListCSV())); } @@ -5427,21 +5441,32 @@ void MainWindow::updateDownloadView() void MainWindow::modDetailsUpdated(bool) { - if (--m_ModsToUpdate == 0) { + if (m_ModsToUpdate <= 0) { statusBar()->hide(); - m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)); - for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) { - if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { - ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i)); - break; - } - } m_RefreshProgress->setVisible(false); } else { m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); } } +void MainWindow::modUpdateCheck() +{ + if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { + m_ModsToUpdate += ModInfo::autoUpdateCheck(&m_PluginContainer, this); + m_RefreshProgress->setRange(0, m_ModsToUpdate); + } else { + QString apiKey; + if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { + m_OrganizerCore.doAfterLogin([this]() { this->checkModsForUpdates(); }); + NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); + } else { // otherwise there will be no endorsement info + MessageDialog::showMessage(tr("Not logged in, endorsement information will be wrong"), + this, true); + m_ModsToUpdate += ModInfo::autoUpdateCheck(&m_PluginContainer, this); + } + } +} + void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) { QVariantMap resultInfo = resultData.toMap(); @@ -5458,12 +5483,7 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD } } std::vector modsList = ModInfo::getByModID(gameNameReal, modID); - // Not clear to me what this is accomplishing? - //if (sameNexus) { - // std::vector mainInfo = ModInfo::getByModID(m_OrganizerCore.managedGame()->gameShortName(), modID); - // info.reserve(info.size() + mainInfo.size()); - // info.insert(info.end(), mainInfo.begin(), mainInfo.end()); - //} + for (auto mod : modsList) { QString installedFile = mod->getInstallationFile(); for (auto update : fileUpdates) { @@ -5511,6 +5531,7 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD if (foundUpdate) { // Just get the standard data updates for endorsements and descriptions + mod->setLastNexusUpdate(QDateTime::currentDateTimeUtc()); mod->updateNXMInfo(); } else { // Scrape mod data here so we can use the mod version if no file update was located @@ -5518,29 +5539,9 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD } } - // Old endorsement and mod info updater - //for - // if (updateData["old_file_id"].toInt() == mod->readMeta()) - // (*iter)->setNewestVersion(result["version"].toString()); - //(*iter)->setNexusDescription(result["description"].toString()); - //if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated() && - // result.contains("voted_by_user") && - // Settings::instance().endorsementIntegration()) { - // // don't use endorsement info if we're not logged in or if the response doesn't contain it - // (*iter)->setIsEndorsed(result["voted_by_user"].toBool()); - //} - - if (m_ModsToUpdate <= 0) { + if (--m_ModsToUpdate <= 0) { statusBar()->hide(); - m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)); - for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) { - if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { - ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i)); - break; - } - } - } - else { + } else { m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); } } @@ -5570,9 +5571,8 @@ void MainWindow::nxmDescriptionAvailable(QString gameName, int modID, QVariant u else mod->setIsEndorsed(false); } + mod->saveMeta(); } - disconnect(sender(), SIGNAL(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)), - this, SLOT(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int))); } void MainWindow::nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int) diff --git a/src/mainwindow.h b/src/mainwindow.h index b3490d07..65ca99dd 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -348,6 +348,7 @@ private: QTimer m_CheckBSATimer; QTimer m_SaveMetaTimer; QTimer m_UpdateProblemsTimer; + QTimer m_ModUpdateTimer; QFuture m_MetaSave; @@ -508,6 +509,8 @@ private slots: void modInstalled(const QString &modName); + void modUpdateCheck(); + void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int); diff --git a/src/modinfo.cpp b/src/modinfo.cpp index e7b7657b..5d5cb57e 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -295,20 +295,63 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv // NexusInterface::instance(pluginContainer)->requestUpdates(game->nexusModOrganizerID(), receiver, QVariant(), game->gameShortName(), QString()); //} - std::multimap> organizedGames; + std::multimap organizedGames; for (auto mod : s_Collection) { if (mod->canBeUpdated()) { - organizedGames.insert(std::pair>(mod->getGameName(), mod)); + organizedGames.insert(std::make_pair(mod->getGameName(), mod->getNexusID())); } } + result = organizedGames.size(); + for (auto game : organizedGames) { - NexusInterface::instance(pluginContainer)->requestUpdates(game.second->getNexusID(), receiver, QVariant(), game.first, QString()); + NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString()); + } + + return result; +} + + +int ModInfo::autoUpdateCheck(PluginContainer *pluginContainer, QObject *receiver) +{ + qInfo("Initializing periodic update check."); + int result = 0; + + std::vector> sortedMods; + std::multimap organizedGames; + for (auto mod : s_Collection) { + if (mod->canBeUpdated()) { + sortedMods.push_back(mod); + } + } + + std::sort(sortedMods.begin(), sortedMods.end(), [](QSharedPointer a, QSharedPointer b) -> bool { + return a->getLastNexusUpdate() > b->getLastNexusUpdate(); + }); + + if (sortedMods.size() > 10) + sortedMods.resize(10); + + result = sortedMods.size(); + + if (sortedMods.size()) { + qInfo("Checking updates for %d mods...", sortedMods.size()); + + for (auto mod : sortedMods) { + organizedGames.insert(std::make_pair(mod->getGameName(), mod->getNexusID())); + } + + for (auto game : organizedGames) { + NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString()); + } + } else { + qInfo("No mods require updates at this time."); } return result; } + void ModInfo::setVersion(const VersionInfo &version) { m_Version = version; diff --git a/src/modinfo.h b/src/modinfo.h index 6b7c714e..9c753752 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -1,710 +1,723 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#ifndef MODINFO_H -#define MODINFO_H - -#include "imodinterface.h" -#include "versioninfo.h" - -class PluginContainer; - -class QDateTime; -class QDir; -#include -#include -#include -#include -#include - -#include - -#include -#include -#include - -namespace MOBase { class IPluginGame; } -namespace MOShared { class DirectoryEntry; } - -/** - * @brief Represents meta information about a single mod. - * - * Represents meta information about a single mod. The class interface is used - * to manage the mod collection - * - **/ -class ModInfo : public QObject, public MOBase::IModInterface -{ - - Q_OBJECT - -public: - - typedef QSharedPointer Ptr; - - static QString s_HiddenExt; - - enum EFlag { - FLAG_INVALID, - FLAG_BACKUP, - FLAG_SEPARATOR, - FLAG_OVERWRITE, - FLAG_FOREIGN, - FLAG_NOTENDORSED, - FLAG_NOTES, - FLAG_CONFLICT_OVERWRITE, - FLAG_CONFLICT_OVERWRITTEN, - FLAG_CONFLICT_MIXED, - FLAG_CONFLICT_REDUNDANT, - FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE, - FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN, - FLAG_ARCHIVE_CONFLICT_OVERWRITE, - FLAG_ARCHIVE_CONFLICT_OVERWRITTEN, - FLAG_ARCHIVE_CONFLICT_MIXED, - FLAG_PLUGIN_SELECTED, - FLAG_ALTERNATE_GAME - }; - - enum EContent { - CONTENT_PLUGIN, - CONTENT_TEXTURE, - CONTENT_MESH, - CONTENT_BSA, - CONTENT_INTERFACE, - CONTENT_SOUND, - CONTENT_SCRIPT, - CONTENT_SKSE, - CONTENT_SKSEFILES, - CONTENT_SKYPROC, - CONTENT_MCM, - CONTENT_INI, - CONTENT_MODGROUP - }; - - static const int NUM_CONTENT_TYPES = CONTENT_MODGROUP + 1; - - enum EHighlight { - HIGHLIGHT_NONE = 0, - HIGHLIGHT_INVALID = 1, - HIGHLIGHT_CENTER = 2, - HIGHLIGHT_IMPORTANT = 4, - HIGHLIGHT_PLUGIN = 8 - }; - - enum EEndorsedState { - ENDORSED_FALSE, - ENDORSED_TRUE, - ENDORSED_UNKNOWN, - ENDORSED_NEVER - }; - - enum EModType { - MOD_DEFAULT, - MOD_DLC, - MOD_CC - }; - - -public: - - /** - * @brief read the mod directory and Mod ModInfo objects for all subdirectories - **/ - static void updateFromDisc(const QString &modDirectory, - MOShared::DirectoryEntry **directoryStructure, - PluginContainer *pluginContainer, - bool displayForeign, - MOBase::IPluginGame const *game); - - static void clear() { s_Collection.clear(); s_ModsByName.clear(); s_ModsByModID.clear(); } - - /** - * @brief retrieve the number of mods - * - * @return number of mods - **/ - static unsigned int getNumMods(); - - /** - * @brief retrieve a ModInfo object based on its index - * - * @param index the index to look up. the maximum is getNumMods() - 1 - * @return a reference counting pointer to the mod info. - * @note since the pointer is reference counting, the pointer remains valid even if the collection is refreshed in a different thread - **/ - static ModInfo::Ptr getByIndex(unsigned int index); - - /** - * @brief retrieve a ModInfo object based on its nexus mod id - * - * @param modID the nexus mod id to look up - * @return a reference counting pointer to the mod info - * @todo in its current form, this function is broken! There may be multiple mods with the same nexus id, - * this function will return only one of them - **/ - static std::vector getByModID(QString game, int modID); - - /** - * @brief remove a mod by index - * - * this physically deletes the specified mod from the disc and updates the ModInfo collection - * but not other structures that reference mods - * @param index index of the mod to delete - * @return true if removal was successful, fals otherwise - **/ - static bool removeMod(unsigned int index); - - /** - * @brief retrieve the mod index by the mod name - * - * @param name name of the mod to look up - * @return the index of the mod. If the mod doesn't exist, UINT_MAX is returned - **/ - static unsigned int getIndex(const QString &name); - - /** - * @brief find the first mod that fulfills the filter function (after no particular order) - * @param filter a function to filter by. should return true for a match - * @return index of the matching mod or UINT_MAX if there wasn't a match - */ - static unsigned int findMod(const boost::function &filter); - - /** - * @brief check a bunch of mods for updates - * @param modIDs list of mods (Nexus Mod IDs) to check for updates - * @return - */ - static void checkChunkForUpdate(PluginContainer *pluginContainer, const std::vector &modIDs, QObject *receiver, QString gameName); - - /** - * @brief query nexus information for every mod and update the "newest version" information - **/ - static int checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver); - - /** - * @brief create a new mod from the specified directory and add it to the collection - * @param dir directory to create from - * @return pointer to the info-structure of the newly created/added mod - */ - static ModInfo::Ptr createFrom(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, const QDir &dir, MOShared::DirectoryEntry **directoryStructure); - - /** - * @brief create a new "foreign-managed" mod from a tuple of plugin and archives - * @param espName name of the plugin - * @param bsaNames names of archives - * @return a new mod - */ - static ModInfo::Ptr createFromPlugin(const QString &modName, const QString &espName, const QStringList &bsaNames, ModInfo::EModType modType, MOShared::DirectoryEntry **directoryStructure, PluginContainer *pluginContainer); - - /** - * @brief retieve a name for one of the CONTENT_ enums - * @param contentType the content value - * @return a display string - */ - static QString getContentTypeName(int contentType); - - virtual bool isRegular() const { return false; } - - virtual bool isEmpty() const { return false; } - - /** - * @brief test if there is a newer version of the mod - * - * test if there is a newer version of the mod. This does NOT cause - * information to be retrieved from the nexus, it will only test version information already - * available locally. Use checkAllForUpdate() to update this version information - * - * @return true if there is a newer version - **/ - virtual bool updateAvailable() const = 0; - - /** - * @return true if the update currently available is ignored - */ - virtual bool updateIgnored() const = 0; - - /** - * @brief test if the "newest" version of the mod is older than the installed version - * - * test if there is a newer version of the mod. This does NOT cause - * information to be retrieved from the nexus, it will only test version information already - * available locally. Use checkAllForUpdate() to update this version information - * - * @return true if the newest version is older than the installed one - **/ - virtual bool downgradeAvailable() const = 0; - - /** - * @brief request an update of nexus description for this mod. - * - * This requests mod information from the nexus. This is an asynchronous request, - * so there is no immediate effect of this call. - * Right now, Mod Organizer interprets the "newest version" and "description" from the - * response, though the description is only stored in memory - * - **/ - virtual bool updateNXMInfo() = 0; - - /** - * @brief assign or unassign the specified category - * - * Every mod can have an arbitrary number of categories assigned to it - * - * @param categoryID id of the category to set - * @param active determines wheter the category is assigned or unassigned - * @note this function does not test whether categoryID actually identifies a valid category - **/ - virtual void setCategory(int categoryID, bool active) = 0; - - /** - * @brief changes the comments (manually set information displayed in the mod list) for this mod - * @param comments new comments - */ - virtual void setComments(const QString &comments) = 0; - - /** - * @brief change the notes (manually set information) for this mod - * @param notes new notes - */ - virtual void setNotes(const QString ¬es) = 0; - - /** - * @brief set/change the source game of this mod - * - * @param gameName the source game shortName - */ - virtual void setGameName(QString gameName) = 0; - - /** - * @brief set/change the nexus mod id of this mod - * - * @param modID the nexus mod id - **/ - virtual void setNexusID(int modID) = 0; - - /** - * @brief set/change the version of this mod - * @param version new version of the mod - */ - virtual void setVersion(const MOBase::VersionInfo &version); - - /** - * @brief Controls if mod should be highlighted based on plugin selection - * @param isSelected whether or not the plugin has a selected mod - **/ - virtual void setPluginSelected(const bool &isSelected); - - /** - * @brief set the newest version of this mod on the nexus - * - * this can be used to overwrite the version of a mod without actually - * updating the mod - * - * @param version the new version to use - * @todo this function should be made obsolete. All queries for mod information should go through - * this class so no public function for this change is required - **/ - virtual void setNewestVersion(const MOBase::VersionInfo &version) = 0; - - /** - * @brief sets the repository that was used to download the mod - */ - virtual void setRepository(const QString &) {} - - /** - * @brief changes/updates the nexus description text - * @param description the current description text - */ - virtual void setNexusDescription(const QString &description) = 0; - - /** - * @brief sets the file this mod was installed from - * @param fileName name of the file - */ - virtual void setInstallationFile(const QString &fileName) = 0; - - /** - * @brief sets the category id from a nexus category id. Conversion to MO id happens internally - * @param categoryID the nexus category id - * @note if a mapping is not possible, the category is set to the default value - */ - virtual void addNexusCategory(int categoryID) = 0; - - virtual void addCategory(const QString &categoryName) override; - virtual bool removeCategory(const QString &categoryName) override; - virtual QStringList categories() override; - - /** - * update the endorsement state for the mod. This only changes the - * buffered state, it does not sync with Nexus - * @param endorsed the new endorsement state - */ - virtual void setIsEndorsed(bool endorsed) = 0; - - /** - * set the mod to "i don't intend to endorse". The mod will not show as unendorsed but can still be endorsed - */ - virtual void setNeverEndorse() = 0; - - /** - * @brief delete the mod from the disc. This does not update the global ModInfo structure or indices - * @return true if the mod was successfully removed - **/ - virtual bool remove() = 0; - - /** - * @brief endorse or un-endorse the mod. This will sync with nexus! - * @param doEndorse if true, the mod is endorsed, if false, it's un-endorsed. - * @note if doEndorse doesn't differ from the current value, nothing happens. - */ - virtual void endorse(bool doEndorse) = 0; - - /** - * @brief clear all caches held for this mod - */ - virtual void clearCaches() {} - - /** - * @brief getter for the mod name - * - * @return the mod name - **/ - virtual QString name() const = 0; - - /** - * @brief getter for an internal name. This is usually the same as the regular name, but with special mod types it might be - * this is used to distinguish between mods that have the same visible name - * @return internal mod name - */ - virtual QString internalName() const { return name(); } - - /** - * @brief getter for the mod path - * - * @return the (absolute) path to the mod - **/ - virtual QString absolutePath() const = 0; - - /** - * @brief getter for the installation file - * - * @return file used to install this mod from - */ - virtual QString getInstallationFile() const = 0; - - /** - * @return version object for machine based comparisons - **/ - virtual MOBase::VersionInfo getVersion() const { return m_Version; } - - /** - * @brief getter for the newest version number of this mod - * - * @return newest version of the mod - **/ - virtual MOBase::VersionInfo getNewestVersion() const = 0; - - /** - * @return the repository from which the file was downloaded. Only relevant regular mods - */ - virtual QString repository() const { return ""; } - - /** - * @brief ignore the newest version for updates - */ - virtual void ignoreUpdate(bool ignore) = 0; - - /** - * @brief getter for the nexus mod id - * - * @return the nexus mod id. may be 0 if the mod id isn't known or doesn't exist - **/ - virtual int getNexusID() const = 0; - - /** - * @brief getter for the source game repository - * - * @return the source game repository. should default to the active game. - **/ - virtual QString getGameName() const = 0; - - /** - * @return the fixed priority of mods of this type or INT_MIN if the priority of mods - * needs to be user-modifiable. Can be < 0 to force a priority below user-modifable mods - * or INT_MAX to force priority above all user-modifiables - */ - virtual int getFixedPriority() const = 0; - - /** - * @return true if the mod is always enabled - */ - virtual bool alwaysEnabled() const { return false; } - - /** - * @return true if the mod can be updated - */ - virtual bool canBeUpdated() const { return false; } - - /** - * @return true if the mod can be enabled/disabled - */ - virtual bool canBeEnabled() const { return false; } - - /** - * @return a list of flags for this mod - */ - virtual std::vector getFlags() const = 0; - - /** - * @return a list of content types contained in a mod - */ - virtual std::vector getContents() const { return std::vector(); } - - /** - * @brief test if the specified flag is set for this mod - * @param flag the flag to test - * @return true if the flag is set, false otherwise - */ - bool hasFlag(EFlag flag) const; - - /** - * @brief test if the mods contains the specified content - * @param content the content to test - * @return true if the content is there, false otherwise - */ - bool hasContent(ModInfo::EContent content) const; - - /** - * @return an indicator if and how this mod should be highlighted by the UI - */ - virtual int getHighlight() const { return HIGHLIGHT_NONE; } - - /** - * @return list of names of ini tweaks - **/ - virtual std::vector getIniTweaks() const = 0; - - /** - * @return a description about the mod, to be displayed in the ui - */ - virtual QString getDescription() const = 0; - - /** - * @return comments for this mod - */ - virtual QString comments() const = 0; - - /** - * @return notes for this mod - */ - virtual QString notes() const = 0; - - /** - * @return creation time of this mod - */ - virtual QDateTime creationTime() const = 0; - - /** - * @return nexus description of the mod (html) - */ - virtual QString getNexusDescription() const = 0; - - /** - * @return last time nexus was queried for infos on this mod - */ - virtual QDateTime getLastNexusQuery() const = 0; - - /** - * @return a list of files that, if they exist in the data directory are treated as files in THIS mod - */ - virtual QStringList stealFiles() const { return QStringList(); } - - /** - * @return a list of archives belonging to this mod (as absolute file paths) - */ - virtual QStringList archives(bool checkOnDisk = false) = 0; - - /* - *@return the color choosen by the user for the mod/separator - */ - virtual QColor getColor() { return QColor(); } - - /* - *@return true if the color has been set successfully. - */ - virtual void setColor(QColor color) { } - - /** - * @brief adds the information that a file has been installed into this mod - * @param modId id of the mod installed - * @param fileId id of the file installed - */ - virtual void addInstalledFile(int modId, int fileId) = 0; - - /** - * @brief test if the mod belongs to the specified category - * - * @param categoryID the category to test for. - * @return true if the mod belongs to the specified category - * @note this does not verify the id actually identifies a category - **/ - bool categorySet(int categoryID) const; - - /** - * @brief retrive the whole list of categories (as ids) this mod belongs to - * - * @return list of categories - **/ - const std::set &getCategories() const { return m_Categories; } - - /** - * @return id of the primary category of this mod - */ - int getPrimaryCategory() const { return m_PrimaryCategory; } - - /** - * @brief sets the new primary category of the mod - * @param categoryID the category to set - */ - virtual void setPrimaryCategory(int categoryID) { m_PrimaryCategory = categoryID; } - - /** - * @return true if this mod is considered "valid", that is: it contains data used by the game - **/ - virtual bool isValid() const { return m_Valid; } - - /** - * @return true if the file has been endorsed on nexus - */ - virtual EEndorsedState endorsedState() const { return ENDORSED_NEVER; } - - /** - * @brief updates the valid-flag for this mod - */ - void testValid(); - - /** - * @brief updates the mod to flag it as converted in order to ignore the alternate game warning - */ - virtual void markConverted(bool converted) {} - - /** - * @brief updates the mod to flag it as valid in order to ignore the invalid game data flag - */ - virtual void markValidated(bool validated) {} - - /** - * @brief reads meta information from disk - */ - virtual void readMeta() {} - - /** - * @brief stores meta information back to disk - */ - virtual void saveMeta() {} - - /** - * @return retrieve list of mods (as mod index) that are overwritten by this one. Updates may be delayed - */ - virtual std::set getModOverwrite() { return std::set(); } - - /** - * @return list of mods (as mod index) that overwrite this one. Updates may be delayed - */ - virtual std::set getModOverwritten() { return std::set(); } - - /** - * @return retrieve list of mods (as mod index) with archives that are overwritten by this one. Updates may be delayed - */ - virtual std::set getModArchiveOverwrite() { return std::set(); } - - /** - * @return list of mods (as mod index) with archives that overwrite this one. Updates may be delayed - */ - virtual std::set getModArchiveOverwritten() { return std::set(); } - - /** - * @return retrieve list of mods (as mod index) with archives that are overwritten by thos mod's loose files. Updates may be delayed - */ - virtual std::set getModArchiveLooseOverwrite() { return std::set(); } - - /** - * @return list of mods (as mod index) with loose files that overwrite this one's archive files. Updates may be delayed - */ - virtual std::set getModArchiveLooseOverwritten() { return std::set(); } - - /** - * @brief update conflict information - */ - virtual void doConflictCheck() const {} - - /** - * @brief set the URL for a mod - */ - virtual void setURL(QString const &) {} - - /** - * @returns the URL for a mod - */ - virtual QString getURL() const { return ""; } - -signals: - - /** - * @brief emitted whenever the information of a mod changes - * - * @param success true if the mod details were updated successfully, false if not - **/ - void modDetailsUpdated(bool success); - -protected: - - ModInfo(PluginContainer *pluginContainer); - - static void updateIndices(); - static bool ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS); - -private: - - static void createFromOverwrite(PluginContainer *pluginContainer); - -protected: - - static std::vector s_Collection; - static std::map s_ModsByName; - - int m_PrimaryCategory; - std::set m_Categories; - - MOBase::VersionInfo m_Version; - - bool m_PluginSelected = false; - -private: - - static QMutex s_Mutex; - static std::map, std::vector > s_ModsByModID; - static int s_NextID; - - bool m_Valid; - -}; - - -#endif // MODINFO_H +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#ifndef MODINFO_H +#define MODINFO_H + +#include "imodinterface.h" +#include "versioninfo.h" + +class PluginContainer; + +class QDateTime; +class QDir; +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace MOBase { class IPluginGame; } +namespace MOShared { class DirectoryEntry; } + +/** + * @brief Represents meta information about a single mod. + * + * Represents meta information about a single mod. The class interface is used + * to manage the mod collection + * + **/ +class ModInfo : public QObject, public MOBase::IModInterface +{ + + Q_OBJECT + +public: + + typedef QSharedPointer Ptr; + + static QString s_HiddenExt; + + enum EFlag { + FLAG_INVALID, + FLAG_BACKUP, + FLAG_SEPARATOR, + FLAG_OVERWRITE, + FLAG_FOREIGN, + FLAG_NOTENDORSED, + FLAG_NOTES, + FLAG_CONFLICT_OVERWRITE, + FLAG_CONFLICT_OVERWRITTEN, + FLAG_CONFLICT_MIXED, + FLAG_CONFLICT_REDUNDANT, + FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE, + FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN, + FLAG_ARCHIVE_CONFLICT_OVERWRITE, + FLAG_ARCHIVE_CONFLICT_OVERWRITTEN, + FLAG_ARCHIVE_CONFLICT_MIXED, + FLAG_PLUGIN_SELECTED, + FLAG_ALTERNATE_GAME + }; + + enum EContent { + CONTENT_PLUGIN, + CONTENT_TEXTURE, + CONTENT_MESH, + CONTENT_BSA, + CONTENT_INTERFACE, + CONTENT_SOUND, + CONTENT_SCRIPT, + CONTENT_SKSE, + CONTENT_SKSEFILES, + CONTENT_SKYPROC, + CONTENT_MCM, + CONTENT_INI, + CONTENT_MODGROUP + }; + + static const int NUM_CONTENT_TYPES = CONTENT_MODGROUP + 1; + + enum EHighlight { + HIGHLIGHT_NONE = 0, + HIGHLIGHT_INVALID = 1, + HIGHLIGHT_CENTER = 2, + HIGHLIGHT_IMPORTANT = 4, + HIGHLIGHT_PLUGIN = 8 + }; + + enum EEndorsedState { + ENDORSED_FALSE, + ENDORSED_TRUE, + ENDORSED_UNKNOWN, + ENDORSED_NEVER + }; + + enum EModType { + MOD_DEFAULT, + MOD_DLC, + MOD_CC + }; + + +public: + + /** + * @brief read the mod directory and Mod ModInfo objects for all subdirectories + **/ + static void updateFromDisc(const QString &modDirectory, + MOShared::DirectoryEntry **directoryStructure, + PluginContainer *pluginContainer, + bool displayForeign, + MOBase::IPluginGame const *game); + + static void clear() { s_Collection.clear(); s_ModsByName.clear(); s_ModsByModID.clear(); } + + /** + * @brief retrieve the number of mods + * + * @return number of mods + **/ + static unsigned int getNumMods(); + + /** + * @brief retrieve a ModInfo object based on its index + * + * @param index the index to look up. the maximum is getNumMods() - 1 + * @return a reference counting pointer to the mod info. + * @note since the pointer is reference counting, the pointer remains valid even if the collection is refreshed in a different thread + **/ + static ModInfo::Ptr getByIndex(unsigned int index); + + /** + * @brief retrieve a ModInfo object based on its nexus mod id + * + * @param modID the nexus mod id to look up + * @return a reference counting pointer to the mod info + * @todo in its current form, this function is broken! There may be multiple mods with the same nexus id, + * this function will return only one of them + **/ + static std::vector getByModID(QString game, int modID); + + /** + * @brief remove a mod by index + * + * this physically deletes the specified mod from the disc and updates the ModInfo collection + * but not other structures that reference mods + * @param index index of the mod to delete + * @return true if removal was successful, fals otherwise + **/ + static bool removeMod(unsigned int index); + + /** + * @brief retrieve the mod index by the mod name + * + * @param name name of the mod to look up + * @return the index of the mod. If the mod doesn't exist, UINT_MAX is returned + **/ + static unsigned int getIndex(const QString &name); + + /** + * @brief find the first mod that fulfills the filter function (after no particular order) + * @param filter a function to filter by. should return true for a match + * @return index of the matching mod or UINT_MAX if there wasn't a match + */ + static unsigned int findMod(const boost::function &filter); + + /** + * @brief run a limited batch of mod update checks for "newest version" information + */ + static int autoUpdateCheck(PluginContainer *pluginContainer, QObject *receiver); + + /** + * @brief query nexus information for every mod and update the "newest version" information + **/ + static int checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver); + + /** + * @brief create a new mod from the specified directory and add it to the collection + * @param dir directory to create from + * @return pointer to the info-structure of the newly created/added mod + */ + static ModInfo::Ptr createFrom(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, const QDir &dir, MOShared::DirectoryEntry **directoryStructure); + + /** + * @brief create a new "foreign-managed" mod from a tuple of plugin and archives + * @param espName name of the plugin + * @param bsaNames names of archives + * @return a new mod + */ + static ModInfo::Ptr createFromPlugin(const QString &modName, const QString &espName, const QStringList &bsaNames, ModInfo::EModType modType, MOShared::DirectoryEntry **directoryStructure, PluginContainer *pluginContainer); + + /** + * @brief retieve a name for one of the CONTENT_ enums + * @param contentType the content value + * @return a display string + */ + static QString getContentTypeName(int contentType); + + virtual bool isRegular() const { return false; } + + virtual bool isEmpty() const { return false; } + + /** + * @brief test if there is a newer version of the mod + * + * test if there is a newer version of the mod. This does NOT cause + * information to be retrieved from the nexus, it will only test version information already + * available locally. Use checkAllForUpdate() to update this version information + * + * @return true if there is a newer version + **/ + virtual bool updateAvailable() const = 0; + + /** + * @return true if the update currently available is ignored + */ + virtual bool updateIgnored() const = 0; + + /** + * @brief test if the "newest" version of the mod is older than the installed version + * + * test if there is a newer version of the mod. This does NOT cause + * information to be retrieved from the nexus, it will only test version information already + * available locally. Use checkAllForUpdate() to update this version information + * + * @return true if the newest version is older than the installed one + **/ + virtual bool downgradeAvailable() const = 0; + + /** + * @brief request an update of nexus description for this mod. + * + * This requests mod information from the nexus. This is an asynchronous request, + * so there is no immediate effect of this call. + * Right now, Mod Organizer interprets the "newest version" and "description" from the + * response, though the description is only stored in memory + * + **/ + virtual bool updateNXMInfo() = 0; + + /** + * @brief assign or unassign the specified category + * + * Every mod can have an arbitrary number of categories assigned to it + * + * @param categoryID id of the category to set + * @param active determines wheter the category is assigned or unassigned + * @note this function does not test whether categoryID actually identifies a valid category + **/ + virtual void setCategory(int categoryID, bool active) = 0; + + /** + * @brief changes the comments (manually set information displayed in the mod list) for this mod + * @param comments new comments + */ + virtual void setComments(const QString &comments) = 0; + + /** + * @brief change the notes (manually set information) for this mod + * @param notes new notes + */ + virtual void setNotes(const QString ¬es) = 0; + + /** + * @brief set/change the source game of this mod + * + * @param gameName the source game shortName + */ + virtual void setGameName(QString gameName) = 0; + + /** + * @brief set/change the nexus mod id of this mod + * + * @param modID the nexus mod id + **/ + virtual void setNexusID(int modID) = 0; + + /** + * @brief set/change the version of this mod + * @param version new version of the mod + */ + virtual void setVersion(const MOBase::VersionInfo &version); + + /** + * @brief Controls if mod should be highlighted based on plugin selection + * @param isSelected whether or not the plugin has a selected mod + **/ + virtual void setPluginSelected(const bool &isSelected); + + /** + * @brief set the newest version of this mod on the nexus + * + * this can be used to overwrite the version of a mod without actually + * updating the mod + * + * @param version the new version to use + * @todo this function should be made obsolete. All queries for mod information should go through + * this class so no public function for this change is required + **/ + virtual void setNewestVersion(const MOBase::VersionInfo &version) = 0; + + /** + * @brief sets the repository that was used to download the mod + */ + virtual void setRepository(const QString &) {} + + /** + * @brief changes/updates the nexus description text + * @param description the current description text + */ + virtual void setNexusDescription(const QString &description) = 0; + + /** + * @brief sets the file this mod was installed from + * @param fileName name of the file + */ + virtual void setInstallationFile(const QString &fileName) = 0; + + /** + * @brief sets the category id from a nexus category id. Conversion to MO id happens internally + * @param categoryID the nexus category id + * @note if a mapping is not possible, the category is set to the default value + */ + virtual void addNexusCategory(int categoryID) = 0; + + virtual void addCategory(const QString &categoryName) override; + virtual bool removeCategory(const QString &categoryName) override; + virtual QStringList categories() override; + + /** + * update the endorsement state for the mod. This only changes the + * buffered state, it does not sync with Nexus + * @param endorsed the new endorsement state + */ + virtual void setIsEndorsed(bool endorsed) = 0; + + /** + * set the mod to "i don't intend to endorse". The mod will not show as unendorsed but can still be endorsed + */ + virtual void setNeverEndorse() = 0; + + /** + * @brief delete the mod from the disc. This does not update the global ModInfo structure or indices + * @return true if the mod was successfully removed + **/ + virtual bool remove() = 0; + + /** + * @brief endorse or un-endorse the mod. This will sync with nexus! + * @param doEndorse if true, the mod is endorsed, if false, it's un-endorsed. + * @note if doEndorse doesn't differ from the current value, nothing happens. + */ + virtual void endorse(bool doEndorse) = 0; + + /** + * @brief clear all caches held for this mod + */ + virtual void clearCaches() {} + + /** + * @brief getter for the mod name + * + * @return the mod name + **/ + virtual QString name() const = 0; + + /** + * @brief getter for an internal name. This is usually the same as the regular name, but with special mod types it might be + * this is used to distinguish between mods that have the same visible name + * @return internal mod name + */ + virtual QString internalName() const { return name(); } + + /** + * @brief getter for the mod path + * + * @return the (absolute) path to the mod + **/ + virtual QString absolutePath() const = 0; + + /** + * @brief getter for the installation file + * + * @return file used to install this mod from + */ + virtual QString getInstallationFile() const = 0; + + /** + * @return version object for machine based comparisons + **/ + virtual MOBase::VersionInfo getVersion() const { return m_Version; } + + /** + * @brief getter for the newest version number of this mod + * + * @return newest version of the mod + **/ + virtual MOBase::VersionInfo getNewestVersion() const = 0; + + /** + * @return the repository from which the file was downloaded. Only relevant regular mods + */ + virtual QString repository() const { return ""; } + + /** + * @brief ignore the newest version for updates + */ + virtual void ignoreUpdate(bool ignore) = 0; + + /** + * @brief getter for the nexus mod id + * + * @return the nexus mod id. may be 0 if the mod id isn't known or doesn't exist + **/ + virtual int getNexusID() const = 0; + + /** + * @brief getter for the source game repository + * + * @return the source game repository. should default to the active game. + **/ + virtual QString getGameName() const = 0; + + /** + * @return the fixed priority of mods of this type or INT_MIN if the priority of mods + * needs to be user-modifiable. Can be < 0 to force a priority below user-modifable mods + * or INT_MAX to force priority above all user-modifiables + */ + virtual int getFixedPriority() const = 0; + + /** + * @return true if the mod is always enabled + */ + virtual bool alwaysEnabled() const { return false; } + + /** + * @return true if the mod can be updated + */ + virtual bool canBeUpdated() const { return false; } + + /** + * @return true if the mod can be enabled/disabled + */ + virtual bool canBeEnabled() const { return false; } + + /** + * @return a list of flags for this mod + */ + virtual std::vector getFlags() const = 0; + + /** + * @return a list of content types contained in a mod + */ + virtual std::vector getContents() const { return std::vector(); } + + /** + * @brief test if the specified flag is set for this mod + * @param flag the flag to test + * @return true if the flag is set, false otherwise + */ + bool hasFlag(EFlag flag) const; + + /** + * @brief test if the mods contains the specified content + * @param content the content to test + * @return true if the content is there, false otherwise + */ + bool hasContent(ModInfo::EContent content) const; + + /** + * @return an indicator if and how this mod should be highlighted by the UI + */ + virtual int getHighlight() const { return HIGHLIGHT_NONE; } + + /** + * @return list of names of ini tweaks + **/ + virtual std::vector getIniTweaks() const = 0; + + /** + * @return a description about the mod, to be displayed in the ui + */ + virtual QString getDescription() const = 0; + + /** + * @return comments for this mod + */ + virtual QString comments() const = 0; + + /** + * @return notes for this mod + */ + virtual QString notes() const = 0; + + /** + * @return creation time of this mod + */ + virtual QDateTime creationTime() const = 0; + + /** + * @return nexus description of the mod (html) + */ + virtual QString getNexusDescription() const = 0; + + /** + * @brief get the last time nexus was checked for file updates on this mod + */ + virtual QDateTime getLastNexusUpdate() const = 0; + + /** + * @brief set the last time nexus was checked for file updates on this mod + */ + virtual void setLastNexusUpdate(QDateTime time) = 0; + + /** + * @return last time nexus was queried for infos on this mod + */ + virtual QDateTime getLastNexusQuery() const = 0; + + /** + * @brief set the last time nexus was queried for info on this mod + */ + virtual void setLastNexusQuery(QDateTime time) = 0; + + /** + * @return a list of files that, if they exist in the data directory are treated as files in THIS mod + */ + virtual QStringList stealFiles() const { return QStringList(); } + + /** + * @return a list of archives belonging to this mod (as absolute file paths) + */ + virtual QStringList archives(bool checkOnDisk = false) = 0; + + /* + *@return the color choosen by the user for the mod/separator + */ + virtual QColor getColor() { return QColor(); } + + /* + *@return true if the color has been set successfully. + */ + virtual void setColor(QColor color) { } + + /** + * @brief adds the information that a file has been installed into this mod + * @param modId id of the mod installed + * @param fileId id of the file installed + */ + virtual void addInstalledFile(int modId, int fileId) = 0; + + /** + * @brief test if the mod belongs to the specified category + * + * @param categoryID the category to test for. + * @return true if the mod belongs to the specified category + * @note this does not verify the id actually identifies a category + **/ + bool categorySet(int categoryID) const; + + /** + * @brief retrive the whole list of categories (as ids) this mod belongs to + * + * @return list of categories + **/ + const std::set &getCategories() const { return m_Categories; } + + /** + * @return id of the primary category of this mod + */ + int getPrimaryCategory() const { return m_PrimaryCategory; } + + /** + * @brief sets the new primary category of the mod + * @param categoryID the category to set + */ + virtual void setPrimaryCategory(int categoryID) { m_PrimaryCategory = categoryID; } + + /** + * @return true if this mod is considered "valid", that is: it contains data used by the game + **/ + virtual bool isValid() const { return m_Valid; } + + /** + * @return true if the file has been endorsed on nexus + */ + virtual EEndorsedState endorsedState() const { return ENDORSED_NEVER; } + + /** + * @brief updates the valid-flag for this mod + */ + void testValid(); + + /** + * @brief updates the mod to flag it as converted in order to ignore the alternate game warning + */ + virtual void markConverted(bool converted) {} + + /** + * @brief updates the mod to flag it as valid in order to ignore the invalid game data flag + */ + virtual void markValidated(bool validated) {} + + /** + * @brief reads meta information from disk + */ + virtual void readMeta() {} + + /** + * @brief stores meta information back to disk + */ + virtual void saveMeta() {} + + /** + * @return retrieve list of mods (as mod index) that are overwritten by this one. Updates may be delayed + */ + virtual std::set getModOverwrite() { return std::set(); } + + /** + * @return list of mods (as mod index) that overwrite this one. Updates may be delayed + */ + virtual std::set getModOverwritten() { return std::set(); } + + /** + * @return retrieve list of mods (as mod index) with archives that are overwritten by this one. Updates may be delayed + */ + virtual std::set getModArchiveOverwrite() { return std::set(); } + + /** + * @return list of mods (as mod index) with archives that overwrite this one. Updates may be delayed + */ + virtual std::set getModArchiveOverwritten() { return std::set(); } + + /** + * @return retrieve list of mods (as mod index) with archives that are overwritten by thos mod's loose files. Updates may be delayed + */ + virtual std::set getModArchiveLooseOverwrite() { return std::set(); } + + /** + * @return list of mods (as mod index) with loose files that overwrite this one's archive files. Updates may be delayed + */ + virtual std::set getModArchiveLooseOverwritten() { return std::set(); } + + /** + * @brief update conflict information + */ + virtual void doConflictCheck() const {} + + /** + * @brief set the URL for a mod + */ + virtual void setURL(QString const &) {} + + /** + * @returns the URL for a mod + */ + virtual QString getURL() const { return ""; } + +signals: + + /** + * @brief emitted whenever the information of a mod changes + * + * @param success true if the mod details were updated successfully, false if not + **/ + void modDetailsUpdated(bool success); + +protected: + + ModInfo(PluginContainer *pluginContainer); + + static void updateIndices(); + static bool ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS); + +private: + + static void createFromOverwrite(PluginContainer *pluginContainer); + +protected: + + static std::vector s_Collection; + static std::map s_ModsByName; + + int m_PrimaryCategory; + std::set m_Categories; + + MOBase::VersionInfo m_Version; + + bool m_PluginSelected = false; + +private: + + static QMutex s_Mutex; + static std::map, std::vector > s_ModsByModID; + static int s_NextID; + + bool m_Valid; + +}; + + +#endif // MODINFO_H diff --git a/src/modinfodialog.cpp b/src/modinfodialog.cpp index d086de08..db3e5de1 100644 --- a/src/modinfodialog.cpp +++ b/src/modinfodialog.cpp @@ -929,7 +929,7 @@ void ModInfoDialog::activateNexusTab() visitNexusLabel->setToolTip(nexusLink); if (m_ModInfo->getNexusDescription().isEmpty() || - QDateTime::currentDateTime() > m_ModInfo->getLastNexusQuery().addDays(1)) { + QDateTime::currentDateTimeUtc() > m_ModInfo->getLastNexusQuery().addDays(1)) { refreshNexusData(modID); } else { this->modDetailsUpdated(true); diff --git a/src/modinfoforeign.h b/src/modinfoforeign.h index 20bfab2a..0702f268 100644 --- a/src/modinfoforeign.h +++ b/src/modinfoforeign.h @@ -47,7 +47,10 @@ public: virtual std::vector getFlags() const; virtual int getHighlight() const; virtual QString getDescription() const; + virtual QDateTime getLastNexusUpdate() const { return QDateTime(); } + virtual void setLastNexusUpdate(QDateTime time) {} virtual QDateTime getLastNexusQuery() const { return QDateTime(); } + virtual void setLastNexusQuery(QDateTime time) {} virtual QString getNexusDescription() const { return QString(); } virtual int getFixedPriority() const { return INT_MIN; } virtual QStringList archives(bool checkOnDisk = false) { return m_Archives; } diff --git a/src/modinfooverwrite.h b/src/modinfooverwrite.h index b68fb15b..d2882301 100644 --- a/src/modinfooverwrite.h +++ b/src/modinfooverwrite.h @@ -50,7 +50,10 @@ public: virtual std::vector getFlags() const; virtual int getHighlight() const; virtual QString getDescription() const; + virtual QDateTime getLastNexusUpdate() const { return QDateTime(); } + virtual void setLastNexusUpdate(QDateTime time) {} virtual QDateTime getLastNexusQuery() const { return QDateTime(); } + virtual void setLastNexusQuery(QDateTime time) {} virtual QString getNexusDescription() const { return QString(); } virtual QStringList archives(bool checkOnDisk = false); virtual void addInstalledFile(int, int) {} diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 834c09ae..57c4baad 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -98,6 +98,7 @@ void ModInfoRegular::readMeta() m_Validated = metaFile.value("validated", false).toBool(); m_URL = metaFile.value("url", "").toString(); m_LastNexusQuery = QDateTime::fromString(metaFile.value("lastNexusQuery", "").toString(), Qt::ISODate); + m_LastNexusUpdate = QDateTime::fromString(metaFile.value("lastNexusUpdate", "").toString(), Qt::ISODate); m_Color = metaFile.value("color",QColor()).value(); if (metaFile.contains("endorsed")) { if (metaFile.value("endorsed").canConvert()) { @@ -161,6 +162,7 @@ void ModInfoRegular::saveMeta() metaFile.setValue("nexusDescription", m_NexusDescription); metaFile.setValue("url", m_URL); metaFile.setValue("lastNexusQuery", m_LastNexusQuery.toString(Qt::ISODate)); + metaFile.setValue("lastNexusUpdate", m_LastNexusUpdate.toString(Qt::ISODate)); metaFile.setValue("converted", m_Converted); metaFile.setValue("validated", m_Validated); metaFile.setValue("color", m_Color); @@ -225,8 +227,8 @@ void ModInfoRegular::nxmDescriptionAvailable(QString, int, QVariant, QVariant re else setEndorsedState(ENDORSED_FALSE); } - m_LastNexusQuery = QDateTime::currentDateTime(); - //m_MetaInfoChanged = true; + m_LastNexusQuery = QDateTime::currentDateTimeUtc(); + m_MetaInfoChanged = true; saveMeta(); emit modDetailsUpdated(true); } @@ -263,10 +265,14 @@ void ModInfoRegular::nxmRequestFailed(QString, int, int, QVariant userData, QNet bool ModInfoRegular::updateNXMInfo() { - if (m_NexusID > 0) { + QDateTime time = QDateTime::currentDateTimeUtc(); + QDateTime target = m_LastNexusQuery.addSecs(3600); + if (m_NexusID > 0 && time >= target) { m_NexusBridge.requestDescription(m_GameName, m_NexusID, QVariant()); return true; } + QString warning("Please wait until %1 to request updated mod info from Nexus!"); + qWarning() << warning.arg(target.toLocalTime().time().toString(Qt::DefaultLocaleShortDate)); return false; } @@ -483,6 +489,15 @@ void ModInfoRegular::ignoreUpdate(bool ignore) m_MetaInfoChanged = true; } +bool ModInfoRegular::canBeUpdated() const +{ + QDateTime now = QDateTime::currentDateTimeUtc(); + QDateTime target = m_LastNexusUpdate.addSecs(3600); + if (now >= target) + return m_NexusID > 0; + return false; +} + std::vector ModInfoRegular::getFlags() const { @@ -628,11 +643,32 @@ ModInfoRegular::EEndorsedState ModInfoRegular::endorsedState() const return m_EndorsedState; } +QDateTime ModInfoRegular::getLastNexusUpdate() const +{ + return m_LastNexusUpdate; +} + +void ModInfoRegular::setLastNexusUpdate(QDateTime time) +{ + m_LastNexusUpdate = time; + m_MetaInfoChanged = true; + saveMeta(); + emit modDetailsUpdated(true); +} + QDateTime ModInfoRegular::getLastNexusQuery() const { return m_LastNexusQuery; } +void ModInfoRegular::setLastNexusQuery(QDateTime time) +{ + m_LastNexusQuery = time; + m_MetaInfoChanged = true; + saveMeta(); + emit modDetailsUpdated(true); +} + void ModInfoRegular::setURL(QString const &url) { m_URL = url; diff --git a/src/modinforegular.h b/src/modinforegular.h index fdb0e672..8e3952e8 100644 --- a/src/modinforegular.h +++ b/src/modinforegular.h @@ -256,7 +256,7 @@ public: /** * @return true if the mod can be updated */ - virtual bool canBeUpdated() const { return m_NexusID > 0; } + virtual bool canBeUpdated() const; /** * @return true if the mod can be enabled/disabled @@ -315,11 +315,26 @@ public: */ virtual EEndorsedState endorsedState() const; + /** + * @brief get the last time nexus was checked for file updates on this mod + */ + virtual QDateTime getLastNexusUpdate() const; + + /** + * @brief set the last time nexus was checked for file updates on this mod + */ + virtual void setLastNexusUpdate(QDateTime time); + /** * @return last time nexus was queried for infos on this mod */ virtual QDateTime getLastNexusQuery() const; + /** + * @brief set the last time nexus was queried for info on this mod + */ + virtual void setLastNexusQuery(QDateTime time); + virtual QStringList archives(bool checkOnDisk = false); virtual void setColor(QColor color); @@ -376,6 +391,7 @@ private: QDateTime m_CreationTime; QDateTime m_LastNexusQuery; + QDateTime m_LastNexusUpdate; QColor m_Color; diff --git a/src/modinfoseparator.h b/src/modinfoseparator.h index c5e0f0e5..1f3be861 100644 --- a/src/modinfoseparator.h +++ b/src/modinfoseparator.h @@ -43,8 +43,10 @@ public: virtual QString getInstallationFile() const { return ""; } virtual QString getURL() const { return ""; } virtual QString repository() const { return ""; } - + virtual QDateTime getLastNexusUpdate() const { return QDateTime(); } + virtual void setLastNexusUpdate(QDateTime time) {} virtual QDateTime getLastNexusQuery() const { return QDateTime(); } + virtual void setLastNexusQuery(QDateTime time) {} virtual QDateTime creationTime() const { return QDateTime(); } virtual void getNexusFiles diff --git a/src/modlist.cpp b/src/modlist.cpp index daa6ce73..1ec5b556 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -1,1398 +1,1410 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see . -*/ - -#include "modlist.h" - -#include "messagedialog.h" -#include "installationtester.h" -#include "qtgroupingproxy.h" -#include "viewmarkingscrollbar.h" -#include "modlistsortproxy.h" -#include "pluginlist.h" -#include "settings.h" -#include "modinforegular.h" -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - - -using namespace MOBase; - - -ModList::ModList(PluginContainer *pluginContainer, QObject *parent) - : QAbstractItemModel(parent) - , m_Profile(nullptr) - , m_NexusInterface(nullptr) - , m_Modified(false) - , m_FontMetrics(QFont()) - , m_DropOnItems(false) - , m_PluginContainer(pluginContainer) -{ - m_ContentIcons[ModInfo::CONTENT_PLUGIN] = std::make_tuple(":/MO/gui/content/plugin", tr("Game Plugins (ESP/ESM/ESL)")); - m_ContentIcons[ModInfo::CONTENT_INTERFACE] = std::make_tuple(":/MO/gui/content/interface", tr("Interface")); - m_ContentIcons[ModInfo::CONTENT_MESH] = std::make_tuple(":/MO/gui/content/mesh", tr("Meshes")); - m_ContentIcons[ModInfo::CONTENT_BSA] = std::make_tuple(":/MO/gui/content/bsa", tr("Bethesda Archive")); - m_ContentIcons[ModInfo::CONTENT_SCRIPT] = std::make_tuple(":/MO/gui/content/script", tr("Scripts (Papyrus)")); - m_ContentIcons[ModInfo::CONTENT_SKSE] = std::make_tuple(":/MO/gui/content/skse", tr("Script Extender Plugin")); - m_ContentIcons[ModInfo::CONTENT_SKYPROC] = std::make_tuple(":/MO/gui/content/skyproc", tr("SkyProc Patcher")); - m_ContentIcons[ModInfo::CONTENT_SOUND] = std::make_tuple(":/MO/gui/content/sound", tr("Sound or Music")); - m_ContentIcons[ModInfo::CONTENT_TEXTURE] = std::make_tuple(":/MO/gui/content/texture", tr("Textures")); - m_ContentIcons[ModInfo::CONTENT_MCM] = std::make_tuple(":/MO/gui/content/menu", tr("MCM Configuration")); - m_ContentIcons[ModInfo::CONTENT_INI] = std::make_tuple(":/MO/gui/content/inifile", tr("INI files")); - m_ContentIcons[ModInfo::CONTENT_MODGROUP] = std::make_tuple(":/MO/gui/content/modgroup", tr("ModGroup files")); - - m_LastCheck.start(); -} - -ModList::~ModList() -{ - m_ModStateChanged.disconnect_all_slots(); - m_ModMoved.disconnect_all_slots(); -} - -void ModList::setProfile(Profile *profile) -{ - m_Profile = profile; -} - -int ModList::rowCount(const QModelIndex &parent) const -{ - if (!parent.isValid()) { - return ModInfo::getNumMods(); - } else { - return 0; - } -} - -bool ModList::hasChildren(const QModelIndex &parent) const -{ - if (!parent.isValid()) { - return ModInfo::getNumMods() > 0; - } else { - return false; - } -} - - -int ModList::columnCount(const QModelIndex &) const -{ - return COL_LASTCOLUMN + 1; -} - - -QVariant ModList::getOverwriteData(int column, int role) const -{ - switch (role) { - case Qt::DisplayRole: { - if (column == 0) { - return "Overwrite"; - } else { - return QVariant(); - } - } break; - case Qt::CheckStateRole: { - if (column == 0) { - return Qt::Checked; - } else { - return QVariant(); - } - } break; - case Qt::TextAlignmentRole: return QVariant(Qt::AlignCenter | Qt::AlignVCenter); - case Qt::UserRole: return -1; - case Qt::ForegroundRole: return QBrush(Qt::red); - case Qt::ToolTipRole: return tr("This entry contains files that have been created inside the virtual data tree (i.e. by the construction kit)"); - default: return QVariant(); - } -} - - -QString ModList::getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const -{ - switch (flag) { - case ModInfo::FLAG_BACKUP: return tr("Backup"); - case ModInfo::FLAG_SEPARATOR: return tr("Separator"); - case ModInfo::FLAG_INVALID: return tr("No valid game data"); - case ModInfo::FLAG_NOTENDORSED: return tr("Not endorsed yet"); - case ModInfo::FLAG_NOTES: { - QStringList output; - if (!modInfo->comments().isEmpty()) - output << QString("%1").arg(modInfo->comments()); - if (!modInfo->notes().isEmpty()) - output << QString("%1").arg(modInfo->notes()); - return output.join(""); - } - case ModInfo::FLAG_CONFLICT_OVERWRITE: return tr("Overwrites loose files"); - case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return tr("Overwritten loose files"); - case ModInfo::FLAG_CONFLICT_MIXED: return tr("Loose files Overwrites & Overwritten"); - case ModInfo::FLAG_CONFLICT_REDUNDANT: return tr("Redundant"); - case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE: return tr("Overwrites an archive with loose files"); - case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN: return tr("Archive is overwritten by loose files"); - case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE: return tr("Overwrites another archive file"); - case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN: return tr("Overwritten by another archive file"); - case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return tr("Archive files overwrites & overwritten"); - case ModInfo::FLAG_ALTERNATE_GAME: return tr("
This mod is for a different game, " - "make sure it's compatible or it could cause crashes."); - default: return ""; - } -} - - -QVariantList ModList::contentsToIcons(const std::vector &contents) const -{ - QVariantList result; - std::set contentsSet(contents.begin(), contents.end()); - for (auto iter = m_ContentIcons.begin(); iter != m_ContentIcons.end(); ++iter) { - if (contentsSet.find(iter->first) != contentsSet.end()) { - result.append(std::get<0>(iter->second)); - } else { - result.append(QString()); - } - } - return result; -} - -QString ModList::contentsToToolTip(const std::vector &contents) const -{ - QString result(""); - - std::set contentsSet(contents.begin(), contents.end()); - for (auto iter = m_ContentIcons.begin(); iter != m_ContentIcons.end(); ++iter) { - if (contentsSet.find(iter->first) != contentsSet.end()) { - result.append(QString("" - "") - .arg(std::get<0>(iter->second)).arg(std::get<1>(iter->second))); - } - } - result.append("
%2
"); - return result; -} - - -QVariant ModList::data(const QModelIndex &modelIndex, int role) const -{ - if (m_Profile == nullptr) return QVariant(); - if (!modelIndex.isValid()) return QVariant(); - unsigned int modIndex = modelIndex.row(); - int column = modelIndex.column(); - - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - if ((role == Qt::DisplayRole) || - (role == Qt::EditRole)) { - if ((column == COL_FLAGS) - || (column == COL_CONTENT)) { - return QVariant(); - } else if (column == COL_NAME) { - auto flags = modInfo->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) - { - return modInfo->name().replace("_separator", ""); - } - else - return modInfo->name(); - } else if (column == COL_VERSION) { - VersionInfo verInfo = modInfo->getVersion(); - QString version = verInfo.displayString(); - if (role != Qt::EditRole) { - if (version.isEmpty() && modInfo->canBeUpdated()) { - version = "?"; - } - } - return version; - } else if (column == COL_PRIORITY) { - int priority = modInfo->getFixedPriority(); - if (priority != INT_MIN) { - return QVariant(); // hide priority for mods where it's fixed - } else { - return m_Profile->getModPriority(modIndex); - } - } else if (column == COL_MODID) { - int modID = modInfo->getNexusID(); - if (modID >= 0) { - return modID; - } - else { - return QVariant(); - } - } else if (column == COL_GAME) { - if (m_PluginContainer != nullptr) { - for (auto game : m_PluginContainer->plugins()) { - if (game->gameShortName().compare(modInfo->getGameName(), Qt::CaseInsensitive) == 0) - return game->gameName(); - } - } - return modInfo->getGameName(); - } else if (column == COL_CATEGORY) { - if (modInfo->hasFlag(ModInfo::FLAG_FOREIGN)) { - return tr("Non-MO"); - } else { - int category = modInfo->getPrimaryCategory(); - if (category != -1) { - CategoryFactory &categoryFactory = CategoryFactory::instance(); - if (categoryFactory.categoryExists(category)) { - try { - int categoryIdx = categoryFactory.getCategoryIndex(category); - return categoryFactory.getCategoryName(categoryIdx); - } catch (const std::exception &e) { - qCritical("failed to retrieve category name: %s", e.what()); - return QString(); - } - } else { - qWarning("category %d doesn't exist (may have been removed)", category); - modInfo->setCategory(category, false); - return QString(); - } - } else { - return QVariant(); - } - } - } else if (column == COL_INSTALLTIME) { - // display installation time for mods that can be updated - if (modInfo->creationTime().isValid()) { - return modInfo->creationTime(); - } else { - return QVariant(); - } - } else if (column == COL_NOTES) { - return modInfo->comments(); - } else { - return tr("invalid"); - } - } else if ((role == Qt::CheckStateRole) && (column == 0)) { - if (modInfo->canBeEnabled()) { - return m_Profile->modEnabled(modIndex) ? Qt::Checked : Qt::Unchecked; - } else { - return QVariant(); - } - } else if (role == Qt::TextAlignmentRole) { - auto flags = modInfo->getFlags(); - if (column == COL_NAME) { - if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_CENTER) { - return QVariant(Qt::AlignCenter | Qt::AlignVCenter); - } else { - return QVariant(Qt::AlignLeft | Qt::AlignVCenter); - } - } else if (column == COL_VERSION) { - return QVariant(Qt::AlignRight | Qt::AlignVCenter); - } else if (column == COL_NOTES) { - return QVariant(Qt::AlignLeft | Qt::AlignVCenter); - } else { - return QVariant(Qt::AlignCenter | Qt::AlignVCenter); - } - } else if (role == Qt::UserRole) { - if (column == COL_CATEGORY) { - QVariantList categoryNames; - std::set categories = modInfo->getCategories(); - CategoryFactory &categoryFactory = CategoryFactory::instance(); - for (auto iter = categories.begin(); iter != categories.end(); ++iter) { - categoryNames.append(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*iter))); - } - if (categoryNames.count() != 0) { - return categoryNames; - } else { - return QVariant(); - } - } else if (column == COL_PRIORITY) { - int priority = modInfo->getFixedPriority(); - if (priority != INT_MIN) { - return priority; - } else { - return m_Profile->getModPriority(modIndex); - } - } else { - return modInfo->getNexusID(); - } - } else if (role == Qt::UserRole + 1) { - return modIndex; - } else if (role == Qt::UserRole + 2) { - switch (column) { - case COL_MODID: return QtGroupingProxy::AGGR_FIRST; - case COL_VERSION: return QtGroupingProxy::AGGR_MAX; - case COL_CATEGORY: return QtGroupingProxy::AGGR_FIRST; - case COL_PRIORITY: return QtGroupingProxy::AGGR_MIN; - default: return QtGroupingProxy::AGGR_NONE; - } - } else if (role == Qt::UserRole + 3) { - return contentsToIcons(modInfo->getContents()); - } else if (role == Qt::UserRole + 4) { - return modInfo->getGameName(); - } else if (role == Qt::FontRole) { - QFont result; - auto flags = modInfo->getFlags(); - if (column == COL_NAME) { - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) - { - //result.setCapitalization(QFont::AllUppercase); - result.setItalic(true); - //result.setUnderline(true); - result.setBold(true); - } else if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_INVALID) { - result.setItalic(true); - } - } else if ((column == COL_CATEGORY) && (modInfo->hasFlag(ModInfo::FLAG_FOREIGN))) { - result.setItalic(true); - } else if (column == COL_VERSION) { - if (modInfo->updateAvailable() || modInfo->downgradeAvailable()) { - result.setWeight(QFont::Bold); - } - } - return result; - } else if (role == Qt::DecorationRole) { - if (column == COL_VERSION) { - if (modInfo->updateAvailable()) { - return QIcon(":/MO/gui/update_available"); - } else if (modInfo->downgradeAvailable()) { - return QIcon(":/MO/gui/warning"); - } else if (modInfo->getVersion().scheme() == VersionInfo::SCHEME_DATE) { - return QIcon(":/MO/gui/version_date"); - } - } - return QVariant(); - } else if (role == Qt::ForegroundRole) { - if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) && modInfo->getColor().isValid()) { - return Settings::getIdealTextColor(modInfo->getColor()); - } else if (column == COL_NAME) { - int highlight = modInfo->getHighlight(); - if (highlight & ModInfo::HIGHLIGHT_IMPORTANT) - return QBrush(Qt::darkRed); - else if (highlight & ModInfo::HIGHLIGHT_INVALID) - return QBrush(Qt::darkGray); - } else if (column == COL_VERSION) { - if (!modInfo->getNewestVersion().isValid()) { - return QVariant(); - } else if (modInfo->updateAvailable() || modInfo->downgradeAvailable()) { - return QBrush(Qt::red); - } else { - return QBrush(Qt::darkGreen); - } - } - return QVariant(); - } else if ((role == Qt::BackgroundRole) - || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) { - bool overwrite = m_Overwrite.find(modIndex) != m_Overwrite.end(); - bool archiveOverwrite = m_ArchiveOverwrite.find(modIndex) != m_ArchiveOverwrite.end(); - bool archiveLooseOverwrite = m_ArchiveLooseOverwrite.find(modIndex) != m_ArchiveLooseOverwrite.end(); - bool overwritten = m_Overwritten.find(modIndex) != m_Overwritten.end(); - bool archiveOverwritten = m_ArchiveOverwritten.find(modIndex) != m_ArchiveOverwritten.end(); - bool archiveLooseOverwritten = m_ArchiveLooseOverwritten.find(modIndex) != m_ArchiveLooseOverwritten.end(); - if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_PLUGIN) { - return Settings::instance().modlistContainsPluginColor(); - } else if (overwritten || archiveLooseOverwritten) { - return Settings::instance().modlistOverwritingLooseColor(); - } else if (overwrite || archiveLooseOverwrite) { - return Settings::instance().modlistOverwrittenLooseColor(); - } else if (archiveOverwritten) { - return Settings::instance().modlistOverwritingArchiveColor(); - } else if (archiveOverwrite) { - return Settings::instance().modlistOverwrittenArchiveColor(); - } else if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) - && modInfo->getColor().isValid() - && ((role != ViewMarkingScrollBar::DEFAULT_ROLE) - || Settings::instance().colorSeparatorScrollbar())) { - return modInfo->getColor(); - } else { - return QVariant(); - } - } else if (role == Qt::ToolTipRole) { - if (column == COL_FLAGS) { - QString result; - - for (ModInfo::EFlag flag : modInfo->getFlags()) { - if (result.length() != 0) result += "
"; - result += getFlagText(flag, modInfo); - } - - return result; - } else if (column == COL_CONTENT) { - return contentsToToolTip(modInfo->getContents()); - } else if (column == COL_NAME) { - try { - return modInfo->getDescription(); - } catch (const std::exception &e) { - qCritical("invalid mod description: %s", e.what()); - return QString(); - } - } else if (column == COL_VERSION) { - QString text = tr("installed version: \"%1\", newest version: \"%2\"").arg(modInfo->getVersion().displayString(3)).arg(modInfo->getNewestVersion().displayString(3)); - if (modInfo->downgradeAvailable()) { - text += "
" + tr("The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn " - "(i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. " - "Either way you may want to \"upgrade\"."); - } - return text; - } else if (column == COL_CATEGORY) { - const std::set &categories = modInfo->getCategories(); - std::wostringstream categoryString; - categoryString << ToWString(tr("Categories:
")); - CategoryFactory &categoryFactory = CategoryFactory::instance(); - for (std::set::const_iterator catIter = categories.begin(); - catIter != categories.end(); ++catIter) { - if (catIter != categories.begin()) { - categoryString << " , "; - } - try { - categoryString << "" << ToWString(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*catIter))) << ""; - } catch (const std::exception &e) { - qCritical("failed to generate tooltip: %s", e.what()); - return QString(); - } - } - - return ToQString(categoryString.str()); - } else if (column == COL_NOTES) { - return getFlagText(ModInfo::FLAG_NOTES, modInfo); - } else { - return QVariant(); - } - } else { - return QVariant(); - } -} - - -bool ModList::renameMod(int index, const QString &newName) -{ - QString nameFixed = newName; - if (!fixDirectoryName(nameFixed) || nameFixed.isEmpty()) { - MessageDialog::showMessage(tr("Invalid name"), nullptr); - return false; - } - - if (ModList::allMods().contains(nameFixed, Qt::CaseInsensitive) && nameFixed.toLower()!=ModInfo::getByIndex(index)->name().toLower() ) { - MessageDialog::showMessage(tr("Name is already in use by another mod"), nullptr); - return false; - } - - ModInfo::Ptr modInfo = ModInfo::getByIndex(index); - QString oldName = modInfo->name(); - if (nameFixed != oldName) { - // before we rename, ensure there is no scheduled asynchronous to rewrite - m_Profile->cancelModlistWrite(); - - - if (modInfo->setName(nameFixed)) - // Notice there is a good chance that setName() updated the modinfo indexes - // the modRenamed() call will refresh the indexes in the current profile - // and update the modlists in all profiles - emit modRenamed(oldName, nameFixed); - } - - // invalidate the currently displayed state of this list - notifyChange(-1); - return true; -} - -bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) -{ - if (m_Profile == nullptr) return false; - - if (static_cast(index.row()) >= ModInfo::getNumMods()) { - return false; - } - - int modID = index.row(); - - ModInfo::Ptr info = ModInfo::getByIndex(modID); - IModList::ModStates oldState = state(modID); - - bool result = false; - - emit aboutToChangeData(); - - if (role == Qt::CheckStateRole) { - bool enabled = value.toInt() == Qt::Checked; - if (m_Profile->modEnabled(modID) != enabled) { - m_Profile->setModEnabled(modID, enabled); - m_Modified = true; - m_LastCheck.restart(); - emit modlistChanged(index, role); - } - result = true; - emit dataChanged(index, index); - } else if (role == Qt::EditRole) { - switch (index.column()) { - case COL_NAME: { - auto flags = info->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) - { - result = renameMod(modID, value.toString() + "_separator"); - } - else - result = renameMod(modID, value.toString()); - } break; - case COL_PRIORITY: { - bool ok = false; - int newPriority = value.toInt(&ok); - if (ok && newPriority < 0) { - newPriority = 0; - } - if (ok) { - m_Profile->setModPriority(modID, newPriority); - - emit modorder_changed(); - result = true; - } else { - result = false; - } - } break; - case COL_MODID: { - bool ok = false; - int newID = value.toInt(&ok); - if (ok) { - info->setNexusID(newID); - emit modlistChanged(index, role); - result = true; - } else { - result = false; - } - } break; - case COL_VERSION: { - VersionInfo::VersionScheme scheme = info->getVersion().scheme(); - VersionInfo version(value.toString(), scheme, true); - if (version.isValid()) { - info->setVersion(version); - result = true; - } else { - result = false; - } - } break; - case COL_NOTES: { - info->setComments(value.toString()); - result = true; - } break; - default: { - qWarning("edit on column \"%s\" not supported", - getColumnName(index.column()).toUtf8().constData()); - result = false; - } break; - } - if (result) { - emit dataChanged(index, index); - } - } - - emit postDataChanged(); - - IModList::ModStates newState = state(modID); - if (oldState != newState) { - try { - m_ModStateChanged(info->name(), newState); - } catch (const std::exception &e) { - qCritical("failed to invoke state changed notification: %s", e.what()); - } catch (...) { - qCritical("failed to invoke state changed notification: unknown exception"); - } - } - - return result; -} - - -QVariant ModList::headerData(int section, Qt::Orientation orientation, - int role) const -{ - if (orientation == Qt::Horizontal) { - if (role == Qt::DisplayRole) { - return getColumnName(section); - } else if (role == Qt::ToolTipRole) { - return getColumnToolTip(section); - } else if (role == Qt::TextAlignmentRole) { - return QVariant(Qt::AlignCenter); - } else if (role == Qt::SizeHintRole) { - QSize temp = m_FontMetrics.size(Qt::TextSingleLine, getColumnName(section)); - temp.rwidth() += 20; - temp.rheight() += 12; - return temp; - } - } - return QAbstractItemModel::headerData(section, orientation, role); -} - - -Qt::ItemFlags ModList::flags(const QModelIndex &modelIndex) const -{ - Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex); - if (modelIndex.internalId() < 0) { - return Qt::ItemIsEnabled; - } - if (modelIndex.isValid()) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(modelIndex.row()); - std::vector flags = modInfo->getFlags(); - if (modInfo->getFixedPriority() == INT_MIN) { - result |= Qt::ItemIsDragEnabled; - result |= Qt::ItemIsUserCheckable; - if ((modelIndex.column() == COL_PRIORITY) || - (modelIndex.column() == COL_VERSION) || - (modelIndex.column() == COL_MODID)) { - result |= Qt::ItemIsEditable; - } - if (((modelIndex.column() == COL_NAME) || - (modelIndex.column() == COL_NOTES)) - && (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) == flags.end())) { - result |= Qt::ItemIsEditable; - } - } - if (m_DropOnItems - && (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) == flags.end())) { - result |= Qt::ItemIsDropEnabled; - } - } else { - if (!m_DropOnItems) result |= Qt::ItemIsDropEnabled; - } - return result; -} - - -QStringList ModList::mimeTypes() const -{ - QStringList result = QAbstractItemModel::mimeTypes(); - result.append("text/uri-list"); - return result; -} - -QMimeData *ModList::mimeData(const QModelIndexList &indexes) const -{ - QMimeData *result = QAbstractItemModel::mimeData(indexes); - result->setData("text/plain", "mod"); - return result; -} - -void ModList::changeModPriority(std::vector sourceIndices, int newPriority) -{ - if (m_Profile == nullptr) return; - - emit layoutAboutToBeChanged(); - Profile *profile = m_Profile; - - // sort the moving mods by ascending priorities - std::sort(sourceIndices.begin(), sourceIndices.end(), - [profile](const int &LHS, const int &RHS) { - return profile->getModPriority(LHS) > profile->getModPriority(RHS); - }); - - // move mods that are decreasing in priority - for (std::vector::const_iterator iter = sourceIndices.begin(); - iter != sourceIndices.end(); ++iter) { - int oldPriority = profile->getModPriority(*iter); - if (oldPriority > newPriority) { - profile->setModPriority(*iter, newPriority); - m_ModMoved(ModInfo::getByIndex(*iter)->name(), oldPriority, newPriority); - } - } - - // sort the moving mods by descending priorities - std::sort(sourceIndices.begin(), sourceIndices.end(), - [profile](const int &LHS, const int &RHS) { - return profile->getModPriority(LHS) < profile->getModPriority(RHS); - }); - - // if at least one mod is increasing in priority, the target index is - // that of the row BELOW the dropped location, otherwise it's the one above - for (std::vector::const_iterator iter = sourceIndices.begin(); - iter != sourceIndices.end(); ++iter) { - int oldPriority = profile->getModPriority(*iter); - if (oldPriority < newPriority) { - --newPriority; - break; - } - } - - // move mods that are increasing in priority - for (std::vector::const_iterator iter = sourceIndices.begin(); - iter != sourceIndices.end(); ++iter) { - int oldPriority = profile->getModPriority(*iter); - if (oldPriority < newPriority) { - profile->setModPriority(*iter, newPriority); - m_ModMoved(ModInfo::getByIndex(*iter)->name(), oldPriority, newPriority); - } - } - - emit layoutChanged(); - - emit modorder_changed(); -} - - -void ModList::changeModPriority(int sourceIndex, int newPriority) -{ - if (m_Profile == nullptr) return; - emit layoutAboutToBeChanged(); - - m_Profile->setModPriority(sourceIndex, newPriority); - - emit layoutChanged(); - - emit modorder_changed(); -} - -void ModList::setOverwriteMarkers(const std::set &overwrite, const std::set &overwritten) -{ - m_Overwrite = overwrite; - m_Overwritten = overwritten; - notifyChange(0, rowCount() - 1); -} - -void ModList::setArchiveOverwriteMarkers(const std::set &overwrite, const std::set &overwritten) -{ - m_ArchiveOverwrite = overwrite; - m_ArchiveOverwritten = overwritten; - notifyChange(0, rowCount() - 1); -} - -void ModList::setArchiveLooseOverwriteMarkers(const std::set &overwrite, const std::set &overwritten) -{ - m_ArchiveLooseOverwrite = overwrite; - m_ArchiveLooseOverwritten = overwritten; - notifyChange(0, rowCount() - 1); -} - -void ModList::setPluginContainer(PluginContainer *pluginContianer) -{ - m_PluginContainer = pluginContianer; -} - -bool ModList::modInfoAboutToChange(ModInfo::Ptr info) -{ - if (m_ChangeInfo.name.isEmpty()) { - m_ChangeInfo.name = info->name(); - m_ChangeInfo.state = state(info->name()); - return true; - } else { - return false; - } -} - -void ModList::modInfoChanged(ModInfo::Ptr info) -{ - if (info->name() == m_ChangeInfo.name) { - IModList::ModStates newState = state(info->name()); - if (m_ChangeInfo.state != newState) { - m_ModStateChanged(info->name(), newState); - } - - int row = ModInfo::getIndex(info->name()); - info->testValid(); - emit aboutToChangeData(); - emit dataChanged(index(row, 0), index(row, columnCount())); - emit postDataChanged(); - } else { - qCritical("modInfoChanged not called after modInfoAboutToChange"); - } - m_ChangeInfo.name = QString(); -} - -void ModList::disconnectSlots() { - m_ModMoved.disconnect_all_slots(); - m_ModStateChanged.disconnect_all_slots(); -} - -int ModList::timeElapsedSinceLastChecked() const -{ - return m_LastCheck.elapsed(); -} - -void ModList::highlightMods(const QItemSelectionModel *selection, const MOShared::DirectoryEntry &directoryEntry) -{ - for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { - ModInfo::getByIndex(i)->setPluginSelected(false); - } - for (QModelIndex idx : selection->selectedRows(PluginList::COL_NAME)) { - QString modName = idx.data().toString(); - - const MOShared::FileEntry::Ptr fileEntry = directoryEntry.findFile(modName.toStdWString()); - if (fileEntry.get() != nullptr) { - bool archive = false; - std::vector>> origins; - { - std::vector>> alternatives = fileEntry->getAlternatives(); - origins.insert(origins.end(), std::pair>(fileEntry->getOrigin(archive), fileEntry->getArchive())); - } - for (auto originInfo : origins) { - MOShared::FilesOrigin &origin = directoryEntry.getOriginByID(originInfo.first); - for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { - if (ModInfo::getByIndex(i)->internalName() == QString::fromStdWString(origin.getName())) { - ModInfo::getByIndex(i)->setPluginSelected(true); - break; - } - } - } - } - } - notifyChange(0, rowCount() - 1); -} - -IModList::ModStates ModList::state(unsigned int modIndex) const -{ - IModList::ModStates result; - if (modIndex != UINT_MAX) { - result |= IModList::STATE_EXISTS; - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - if (modInfo->isEmpty()) { - result |= IModList::STATE_EMPTY; - } - if (modInfo->endorsedState() == ModInfo::ENDORSED_TRUE) { - result |= IModList::STATE_ENDORSED; - } - if (modInfo->isValid()) { - result |= IModList::STATE_VALID; - } - if (modInfo->isRegular()) { - QSharedPointer modInfoRegular = modInfo.staticCast(); - if (modInfoRegular->isAlternate() && !modInfoRegular->isConverted()) - result |= IModList::STATE_ALTERNATE; - if (!modInfo->isValid() && modInfoRegular->isValidated()) - result |= IModList::STATE_VALID; - } - if (modInfo->canBeEnabled()) { - if (m_Profile->modEnabled(modIndex)) { - result |= IModList::STATE_ACTIVE; - } - } else { - result |= IModList::STATE_ESSENTIAL; - } - } - return result; -} - -QString ModList::displayName(const QString &internalName) const -{ - unsigned int modIndex = ModInfo::getIndex(internalName); - if (modIndex == UINT_MAX) { - // might be better to throw an exception? - return internalName; - } else { - return ModInfo::getByIndex(modIndex)->name(); - } -} - -QStringList ModList::allMods() const -{ - QStringList result; - for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { - result.append(ModInfo::getByIndex(i)->internalName()); - } - return result; -} - -IModList::ModStates ModList::state(const QString &name) const -{ - unsigned int modIndex = ModInfo::getIndex(name); - - return state(modIndex); -} - -bool ModList::setActive(const QString &name, bool active) -{ - unsigned int modIndex = ModInfo::getIndex(name); - if (modIndex == UINT_MAX) { - return false; - } else { - m_Profile->setModEnabled(modIndex, active); - - IModList::ModStates newState = state(modIndex); - m_ModStateChanged(name, newState); - return true; - } -} - -int ModList::priority(const QString &name) const -{ - unsigned int modIndex = ModInfo::getIndex(name); - if (modIndex == UINT_MAX) { - return -1; - } else { - return m_Profile->getModPriority(modIndex); - } -} - -bool ModList::setPriority(const QString &name, int newPriority) -{ - if ((newPriority < 0) || (newPriority >= static_cast(m_Profile->numRegularMods()))) { - return false; - } - - unsigned int modIndex = ModInfo::getIndex(name); - if (modIndex == UINT_MAX) { - return false; - } else { - m_Profile->setModPriority(modIndex, newPriority); - notifyChange(modIndex); - return true; - } -} - -bool ModList::onModStateChanged(const std::function &func) -{ - auto conn = m_ModStateChanged.connect(func); - return conn.connected(); -} - -bool ModList::onModMoved(const std::function &func) -{ - auto conn = m_ModMoved.connect(func); - return conn.connected(); -} - -bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &parent) -{ - QStringList source; - QStringList target; - - if (row == -1) { - row = parent.row(); - } - ModInfo::Ptr modInfo = ModInfo::getByIndex(row); - QDir modDirectory(modInfo->absolutePath()); - QDir gameDirectory(Settings::instance().getOverwriteDirectory()); - - unsigned int overwriteIndex = ModInfo::findMod([] (ModInfo::Ptr mod) -> bool { - std::vector flags = mod->getFlags(); - return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); }); - - QString overwriteName = ModInfo::getByIndex(overwriteIndex)->name(); - - for (const QUrl &url : mimeData->urls()) { - //qDebug("URL drop requested: %s", qUtf8Printable(url.toLocalFile())); - if (!url.isLocalFile()) { - qDebug("URL drop ignored: Not a local file."); - continue; - } - QString relativePath = gameDirectory.relativeFilePath(url.toLocalFile()); - if (relativePath.startsWith("..")) { - qDebug("URL drop ignored: relative path starts with .."); - continue; - } - source.append(url.toLocalFile()); - target.append(modDirectory.absoluteFilePath(relativePath)); - emit fileMoved(relativePath, overwriteName, modInfo->name()); - } - - if (source.count() != 0) { - if (!shellMove(source, target)) { - qDebug("Move failed %lu",::GetLastError()); - return false; - } - } - - if (!modInfo->isValid()) { - modInfo->testValid(); - } - - return true; -} - -bool ModList::dropMod(const QMimeData *mimeData, int row, const QModelIndex &parent) -{ - - try { - QByteArray encoded = mimeData->data("application/x-qabstractitemmodeldatalist"); - QDataStream stream(&encoded, QIODevice::ReadOnly); - std::vector sourceRows; - - while (!stream.atEnd()) { - int sourceRow, col; - QMap roleDataMap; - stream >> sourceRow >> col >> roleDataMap; - if (col == 0) { - sourceRows.push_back(sourceRow); - } - } - - if (row == -1) { - row = parent.row(); - } - - if ((row < 0) || (static_cast(row) >= ModInfo::getNumMods())) { - return false; - } - - int newPriority = 0; - { - if ((row < 0) || (row > static_cast(m_Profile->numRegularMods()))) { - newPriority = m_Profile->numRegularMods() + 1; - } else { - newPriority = m_Profile->getModPriority(row); - } - if (newPriority == -1) { - newPriority = m_Profile->numRegularMods() + 1; - } - } - changeModPriority(sourceRows, newPriority); - } catch (const std::exception &e) { - reportError(tr("drag&drop failed: %1").arg(e.what())); - } - - return false; -} - - -bool ModList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int, const QModelIndex &parent) -{ - if (action == Qt::IgnoreAction) { - return true; - } - - if (m_Profile == nullptr) return false; - - if (mimeData->hasUrls()) { - return dropURLs(mimeData, row, parent); - } else if (mimeData->hasText()) { - return dropMod(mimeData, row, parent); - } else { - return false; - } -} - -void ModList::removeRowForce(int row, const QModelIndex &parent) -{ - if (static_cast(row) >= ModInfo::getNumMods()) { - return; - } - if (m_Profile == nullptr) return; - - ModInfo::Ptr modInfo = ModInfo::getByIndex(row); - - bool wasEnabled = m_Profile->modEnabled(row); - - m_Profile->setModEnabled(row, false); - - m_Profile->cancelModlistWrite(); - beginRemoveRows(parent, row, row); - ModInfo::removeMod(row); - endRemoveRows(); - m_Profile->refreshModStatus(); // removes the mod from the status list - m_Profile->writeModlist(); // this ensures the modified list gets written back before new mods can be installed - - if (wasEnabled) { - emit removeOrigin(modInfo->name()); - } - - emit modUninstalled(modInfo->getInstallationFile()); -} - -bool ModList::removeRows(int row, int count, const QModelIndex &parent) -{ - if (static_cast(row) >= ModInfo::getNumMods()) { - return false; - } - if (m_Profile == nullptr) { - return false; - } - - bool success = false; - - if (count == 1) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(row); - std::vector flags = modInfo->getFlags(); - if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) && (QDir(modInfo->absolutePath()).count() > 2)) { - emit clearOverwrite(); - success = true; - } - } - - for (int i = 0; i < count; ++i) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(row + i); - if (!modInfo->isRegular()) { - continue; - } - - success = true; - - QMessageBox confirmBox(QMessageBox::Question, tr("Confirm"), - tr("Are you sure you want to remove \"%1\"?").arg(modInfo->name()), - QMessageBox::Yes | QMessageBox::No); - - if (confirmBox.exec() == QMessageBox::Yes) { - m_Profile->setModEnabled(row + i, false); - removeRowForce(row + i, parent); - } - } - - return success; -} - - -void ModList::notifyChange(int rowStart, int rowEnd) -{ - if (rowStart < 0) { - m_Overwrite.clear(); - m_Overwritten.clear(); - m_ArchiveOverwrite.clear(); - m_ArchiveOverwritten.clear(); - m_ArchiveLooseOverwrite.clear(); - m_ArchiveLooseOverwritten.clear(); - beginResetModel(); - endResetModel(); - } else { - if (rowEnd == -1) { - rowEnd = rowStart; - } - emit dataChanged(this->index(rowStart, 0), this->index(rowEnd, this->columnCount() - 1)); - } -} - - -QModelIndex ModList::index(int row, int column, const QModelIndex&) const -{ - if ((row < 0) || (row >= rowCount()) || (column < 0) || (column >= columnCount())) { - return QModelIndex(); - } - QModelIndex res = createIndex(row, column, row); - return res; -} - - -QModelIndex ModList::parent(const QModelIndex&) const -{ - return QModelIndex(); -} - -QMap ModList::itemData(const QModelIndex &index) const -{ - QMap result = QAbstractItemModel::itemData(index); - result[Qt::UserRole] = data(index, Qt::UserRole); - return result; -} - - -void ModList::dropModeUpdate(bool dropOnItems) -{ - if (m_DropOnItems != dropOnItems) { - m_DropOnItems = dropOnItems; - } -} - - -QString ModList::getColumnName(int column) -{ - switch (column) { - case COL_FLAGS: return tr("Flags"); - case COL_CONTENT: return tr("Content"); - case COL_NAME: return tr("Mod Name"); - case COL_VERSION: return tr("Version"); - case COL_PRIORITY: return tr("Priority"); - case COL_CATEGORY: return tr("Category"); - case COL_GAME: return tr("Source Game"); - case COL_MODID: return tr("Nexus ID"); - case COL_INSTALLTIME: return tr("Installation"); - case COL_NOTES: return tr("Notes"); - default: return tr("unknown"); - } -} - - -QString ModList::getColumnToolTip(int column) -{ - switch (column) { - case COL_NAME: return tr("Name of your mods"); - case COL_VERSION: return tr("Version of the mod (if available)"); - case COL_PRIORITY: return tr("Installation priority of your mod. The higher, the more \"important\" it is and thus " - "overwrites files from mods with lower priority."); - case COL_CATEGORY: return tr("Category of the mod."); - case COL_GAME: return tr("The source game which was the origin of this mod."); - case COL_MODID: return tr("Id of the mod as used on Nexus."); - case COL_FLAGS: return tr("Emblemes to highlight things that might require attention."); - case COL_CONTENT: return tr("Depicts the content of the mod:
" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - - "
Game plugins (esp/esm/esl)
Interface
Meshes
BSA
Textures
Sounds
Music
Strings
Scripts (Papyrus)
Script Extender plugins
SkyProc Patcher
Mod Configuration Menu
INI files
ModGroup files
"); - case COL_INSTALLTIME: return tr("Time this mod was installed"); - case COL_NOTES: return tr("User notes about the mod"); - default: return tr("unknown"); - } -} - - -bool ModList::moveSelection(QAbstractItemView *itemView, int direction) -{ - QItemSelectionModel *selectionModel = itemView->selectionModel(); - - const QAbstractProxyModel *proxyModel = qobject_cast(selectionModel->model()); - const QSortFilterProxyModel *filterModel = nullptr; - - while ((filterModel == nullptr) && (proxyModel != nullptr)) { - filterModel = qobject_cast(proxyModel); - if (filterModel == nullptr) { - proxyModel = qobject_cast(proxyModel->sourceModel()); - } - } - if (filterModel == nullptr) { - return true; - } - - int offset = -1; - if (((direction < 0) && (filterModel->sortOrder() == Qt::DescendingOrder)) || - ((direction > 0) && (filterModel->sortOrder() == Qt::AscendingOrder))) { - offset = 1; - } - - QModelIndexList rows = selectionModel->selectedRows(); - if (direction > 0) { - for (int i = 0; i < rows.size() / 2; ++i) { - rows.swap(i, rows.size() - i - 1); - } - } - for (QModelIndex idx : rows) { - if (filterModel != nullptr) { - idx = filterModel->mapToSource(idx); - } - int newPriority = m_Profile->getModPriority(idx.row()) + offset; - if ((newPriority >= 0) && (newPriority < static_cast(m_Profile->numRegularMods()))) { - m_Profile->setModPriority(idx.row(), newPriority); - notifyChange(idx.row()); - } - } - emit modorder_changed(); - return true; -} - -bool ModList::deleteSelection(QAbstractItemView *itemView) -{ - QItemSelectionModel *selectionModel = itemView->selectionModel(); - - QModelIndexList rows = selectionModel->selectedRows(); - if (rows.count() > 1) { - emit removeSelectedMods(); - } else if (rows.count() == 1) { - removeRow(rows[0].data(Qt::UserRole + 1).toInt(), QModelIndex()); - } - return true; -} - -bool ModList::toggleSelection(QAbstractItemView *itemView) -{ - emit aboutToChangeData(); - - QItemSelectionModel *selectionModel = itemView->selectionModel(); - - QList modsToEnable; - QList modsToDisable; - QModelIndexList dirtyMods; - for (QModelIndex idx : selectionModel->selectedRows()) { - int modId = idx.data(Qt::UserRole + 1).toInt(); - if (m_Profile->modEnabled(modId)) { - modsToDisable.append(modId); - dirtyMods.append(idx); - } else { - modsToEnable.append(modId); - dirtyMods.append(idx); - } - } - - m_Profile->setModsEnabled(modsToEnable, modsToDisable); - - emit modlistChanged(dirtyMods, 0); - - m_Modified = true; - m_LastCheck.restart(); - - emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1)); - - emit postDataChanged(); - - return true; -} - -bool ModList::eventFilter(QObject *obj, QEvent *event) -{ - if (event->type() == QEvent::ContextMenu) { - QContextMenuEvent *contextEvent = static_cast(event); - QWidget *object = qobject_cast(obj); - if ((object != nullptr) && (contextEvent->reason() == QContextMenuEvent::Mouse)) { - emit requestColumnSelect(object->mapToGlobal(contextEvent->pos())); - - return true; - } - } else if ((event->type() == QEvent::KeyPress) && (m_Profile != nullptr)) { - QAbstractItemView *itemView = qobject_cast(obj); - QKeyEvent *keyEvent = static_cast(event); - - if ((itemView != nullptr) - && (keyEvent->modifiers() == Qt::ControlModifier) - && ((keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down))) { - return moveSelection(itemView, keyEvent->key() == Qt::Key_Up ? -1 : 1); - } else if (keyEvent->key() == Qt::Key_Delete) { - return deleteSelection(itemView); - } else if (keyEvent->key() == Qt::Key_Space) { - return toggleSelection(itemView); - } - return QAbstractItemModel::eventFilter(obj, event); - } - return QAbstractItemModel::eventFilter(obj, event); -} - -//note: caller needs to make sure sort proxy is updated -void ModList::enableSelected(const QItemSelectionModel *selectionModel) -{ - if (selectionModel->hasSelection()) { - QList modsToEnable; - for (auto row : selectionModel->selectedRows(COL_PRIORITY)) { - int modID = m_Profile->modIndexByPriority(row.data().toInt()); - modsToEnable.append(modID); - } - m_Profile->setModsEnabled(modsToEnable, QList()); - } -} - -//note: caller needs to make sure sort proxy is updated -void ModList::disableSelected(const QItemSelectionModel *selectionModel) -{ - if (selectionModel->hasSelection()) { - QList modsToDisable; - for (auto row : selectionModel->selectedRows(COL_PRIORITY)) { - int modID = m_Profile->modIndexByPriority(row.data().toInt()); - modsToDisable.append(modID); - } - m_Profile->setModsEnabled(QList(), modsToDisable); - } -} +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "modlist.h" + +#include "messagedialog.h" +#include "installationtester.h" +#include "qtgroupingproxy.h" +#include "viewmarkingscrollbar.h" +#include "modlistsortproxy.h" +#include "pluginlist.h" +#include "settings.h" +#include "modinforegular.h" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + +using namespace MOBase; + + +ModList::ModList(PluginContainer *pluginContainer, QObject *parent) + : QAbstractItemModel(parent) + , m_Profile(nullptr) + , m_NexusInterface(nullptr) + , m_Modified(false) + , m_FontMetrics(QFont()) + , m_DropOnItems(false) + , m_PluginContainer(pluginContainer) +{ + m_ContentIcons[ModInfo::CONTENT_PLUGIN] = std::make_tuple(":/MO/gui/content/plugin", tr("Game Plugins (ESP/ESM/ESL)")); + m_ContentIcons[ModInfo::CONTENT_INTERFACE] = std::make_tuple(":/MO/gui/content/interface", tr("Interface")); + m_ContentIcons[ModInfo::CONTENT_MESH] = std::make_tuple(":/MO/gui/content/mesh", tr("Meshes")); + m_ContentIcons[ModInfo::CONTENT_BSA] = std::make_tuple(":/MO/gui/content/bsa", tr("Bethesda Archive")); + m_ContentIcons[ModInfo::CONTENT_SCRIPT] = std::make_tuple(":/MO/gui/content/script", tr("Scripts (Papyrus)")); + m_ContentIcons[ModInfo::CONTENT_SKSE] = std::make_tuple(":/MO/gui/content/skse", tr("Script Extender Plugin")); + m_ContentIcons[ModInfo::CONTENT_SKYPROC] = std::make_tuple(":/MO/gui/content/skyproc", tr("SkyProc Patcher")); + m_ContentIcons[ModInfo::CONTENT_SOUND] = std::make_tuple(":/MO/gui/content/sound", tr("Sound or Music")); + m_ContentIcons[ModInfo::CONTENT_TEXTURE] = std::make_tuple(":/MO/gui/content/texture", tr("Textures")); + m_ContentIcons[ModInfo::CONTENT_MCM] = std::make_tuple(":/MO/gui/content/menu", tr("MCM Configuration")); + m_ContentIcons[ModInfo::CONTENT_INI] = std::make_tuple(":/MO/gui/content/inifile", tr("INI files")); + m_ContentIcons[ModInfo::CONTENT_MODGROUP] = std::make_tuple(":/MO/gui/content/modgroup", tr("ModGroup files")); + + m_LastCheck.start(); +} + +ModList::~ModList() +{ + m_ModStateChanged.disconnect_all_slots(); + m_ModMoved.disconnect_all_slots(); +} + +void ModList::setProfile(Profile *profile) +{ + m_Profile = profile; +} + +int ModList::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return ModInfo::getNumMods(); + } else { + return 0; + } +} + +bool ModList::hasChildren(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return ModInfo::getNumMods() > 0; + } else { + return false; + } +} + + +int ModList::columnCount(const QModelIndex &) const +{ + return COL_LASTCOLUMN + 1; +} + + +QVariant ModList::getOverwriteData(int column, int role) const +{ + switch (role) { + case Qt::DisplayRole: { + if (column == 0) { + return "Overwrite"; + } else { + return QVariant(); + } + } break; + case Qt::CheckStateRole: { + if (column == 0) { + return Qt::Checked; + } else { + return QVariant(); + } + } break; + case Qt::TextAlignmentRole: return QVariant(Qt::AlignCenter | Qt::AlignVCenter); + case Qt::UserRole: return -1; + case Qt::ForegroundRole: return QBrush(Qt::red); + case Qt::ToolTipRole: return tr("This entry contains files that have been created inside the virtual data tree (i.e. by the construction kit)"); + default: return QVariant(); + } +} + + +QString ModList::getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const +{ + switch (flag) { + case ModInfo::FLAG_BACKUP: return tr("Backup"); + case ModInfo::FLAG_SEPARATOR: return tr("Separator"); + case ModInfo::FLAG_INVALID: return tr("No valid game data"); + case ModInfo::FLAG_NOTENDORSED: return tr("Not endorsed yet"); + case ModInfo::FLAG_NOTES: { + QStringList output; + if (!modInfo->comments().isEmpty()) + output << QString("%1").arg(modInfo->comments()); + if (!modInfo->notes().isEmpty()) + output << QString("%1").arg(modInfo->notes()); + return output.join(""); + } + case ModInfo::FLAG_CONFLICT_OVERWRITE: return tr("Overwrites loose files"); + case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return tr("Overwritten loose files"); + case ModInfo::FLAG_CONFLICT_MIXED: return tr("Loose files Overwrites & Overwritten"); + case ModInfo::FLAG_CONFLICT_REDUNDANT: return tr("Redundant"); + case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE: return tr("Overwrites an archive with loose files"); + case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN: return tr("Archive is overwritten by loose files"); + case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE: return tr("Overwrites another archive file"); + case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN: return tr("Overwritten by another archive file"); + case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return tr("Archive files overwrites & overwritten"); + case ModInfo::FLAG_ALTERNATE_GAME: return tr("
This mod is for a different game, " + "make sure it's compatible or it could cause crashes."); + default: return ""; + } +} + + +QVariantList ModList::contentsToIcons(const std::vector &contents) const +{ + QVariantList result; + std::set contentsSet(contents.begin(), contents.end()); + for (auto iter = m_ContentIcons.begin(); iter != m_ContentIcons.end(); ++iter) { + if (contentsSet.find(iter->first) != contentsSet.end()) { + result.append(std::get<0>(iter->second)); + } else { + result.append(QString()); + } + } + return result; +} + +QString ModList::contentsToToolTip(const std::vector &contents) const +{ + QString result(""); + + std::set contentsSet(contents.begin(), contents.end()); + for (auto iter = m_ContentIcons.begin(); iter != m_ContentIcons.end(); ++iter) { + if (contentsSet.find(iter->first) != contentsSet.end()) { + result.append(QString("" + "") + .arg(std::get<0>(iter->second)).arg(std::get<1>(iter->second))); + } + } + result.append("
%2
"); + return result; +} + + +QVariant ModList::data(const QModelIndex &modelIndex, int role) const +{ + if (m_Profile == nullptr) return QVariant(); + if (!modelIndex.isValid()) return QVariant(); + unsigned int modIndex = modelIndex.row(); + int column = modelIndex.column(); + + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + if ((role == Qt::DisplayRole) || + (role == Qt::EditRole)) { + if ((column == COL_FLAGS) + || (column == COL_CONTENT)) { + return QVariant(); + } else if (column == COL_NAME) { + auto flags = modInfo->getFlags(); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) + { + return modInfo->name().replace("_separator", ""); + } + else + return modInfo->name(); + } else if (column == COL_VERSION) { + VersionInfo verInfo = modInfo->getVersion(); + QString version = verInfo.displayString(); + if (role != Qt::EditRole) { + if (version.isEmpty() && modInfo->canBeUpdated()) { + version = "?"; + } + } + return version; + } else if (column == COL_PRIORITY) { + int priority = modInfo->getFixedPriority(); + if (priority != INT_MIN) { + return QVariant(); // hide priority for mods where it's fixed + } else { + return m_Profile->getModPriority(modIndex); + } + } else if (column == COL_MODID) { + int modID = modInfo->getNexusID(); + if (modID >= 0) { + return modID; + } + else { + return QVariant(); + } + } else if (column == COL_GAME) { + if (m_PluginContainer != nullptr) { + for (auto game : m_PluginContainer->plugins()) { + if (game->gameShortName().compare(modInfo->getGameName(), Qt::CaseInsensitive) == 0) + return game->gameName(); + } + } + return modInfo->getGameName(); + } else if (column == COL_CATEGORY) { + if (modInfo->hasFlag(ModInfo::FLAG_FOREIGN)) { + return tr("Non-MO"); + } else { + int category = modInfo->getPrimaryCategory(); + if (category != -1) { + CategoryFactory &categoryFactory = CategoryFactory::instance(); + if (categoryFactory.categoryExists(category)) { + try { + int categoryIdx = categoryFactory.getCategoryIndex(category); + return categoryFactory.getCategoryName(categoryIdx); + } catch (const std::exception &e) { + qCritical("failed to retrieve category name: %s", e.what()); + return QString(); + } + } else { + qWarning("category %d doesn't exist (may have been removed)", category); + modInfo->setCategory(category, false); + return QString(); + } + } else { + return QVariant(); + } + } + } else if (column == COL_INSTALLTIME) { + // display installation time for mods that can be updated + if (modInfo->creationTime().isValid()) { + return modInfo->creationTime(); + } else { + return QVariant(); + } + } else if (column == COL_NOTES) { + return modInfo->comments(); + } else { + return tr("invalid"); + } + } else if ((role == Qt::CheckStateRole) && (column == 0)) { + if (modInfo->canBeEnabled()) { + return m_Profile->modEnabled(modIndex) ? Qt::Checked : Qt::Unchecked; + } else { + return QVariant(); + } + } else if (role == Qt::TextAlignmentRole) { + auto flags = modInfo->getFlags(); + if (column == COL_NAME) { + if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_CENTER) { + return QVariant(Qt::AlignCenter | Qt::AlignVCenter); + } else { + return QVariant(Qt::AlignLeft | Qt::AlignVCenter); + } + } else if (column == COL_VERSION) { + return QVariant(Qt::AlignRight | Qt::AlignVCenter); + } else if (column == COL_NOTES) { + return QVariant(Qt::AlignLeft | Qt::AlignVCenter); + } else { + return QVariant(Qt::AlignCenter | Qt::AlignVCenter); + } + } else if (role == Qt::UserRole) { + if (column == COL_CATEGORY) { + QVariantList categoryNames; + std::set categories = modInfo->getCategories(); + CategoryFactory &categoryFactory = CategoryFactory::instance(); + for (auto iter = categories.begin(); iter != categories.end(); ++iter) { + categoryNames.append(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*iter))); + } + if (categoryNames.count() != 0) { + return categoryNames; + } else { + return QVariant(); + } + } else if (column == COL_PRIORITY) { + int priority = modInfo->getFixedPriority(); + if (priority != INT_MIN) { + return priority; + } else { + return m_Profile->getModPriority(modIndex); + } + } else { + return modInfo->getNexusID(); + } + } else if (role == Qt::UserRole + 1) { + return modIndex; + } else if (role == Qt::UserRole + 2) { + switch (column) { + case COL_MODID: return QtGroupingProxy::AGGR_FIRST; + case COL_VERSION: return QtGroupingProxy::AGGR_MAX; + case COL_CATEGORY: return QtGroupingProxy::AGGR_FIRST; + case COL_PRIORITY: return QtGroupingProxy::AGGR_MIN; + default: return QtGroupingProxy::AGGR_NONE; + } + } else if (role == Qt::UserRole + 3) { + return contentsToIcons(modInfo->getContents()); + } else if (role == Qt::UserRole + 4) { + return modInfo->getGameName(); + } else if (role == Qt::FontRole) { + QFont result; + auto flags = modInfo->getFlags(); + if (column == COL_NAME) { + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) + { + //result.setCapitalization(QFont::AllUppercase); + result.setItalic(true); + //result.setUnderline(true); + result.setBold(true); + } else if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_INVALID) { + result.setItalic(true); + } + } else if ((column == COL_CATEGORY) && (modInfo->hasFlag(ModInfo::FLAG_FOREIGN))) { + result.setItalic(true); + } else if (column == COL_VERSION) { + if (modInfo->updateAvailable() || modInfo->downgradeAvailable()) { + result.setWeight(QFont::Bold); + } + if (modInfo->canBeUpdated()) { + result.setItalic(true); + } + } + return result; + } else if (role == Qt::DecorationRole) { + if (column == COL_VERSION) { + if (modInfo->updateAvailable()) { + return QIcon(":/MO/gui/update_available"); + } else if (modInfo->downgradeAvailable()) { + return QIcon(":/MO/gui/warning"); + } else if (modInfo->getVersion().scheme() == VersionInfo::SCHEME_DATE) { + return QIcon(":/MO/gui/version_date"); + } + } + return QVariant(); + } else if (role == Qt::ForegroundRole) { + if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) && modInfo->getColor().isValid()) { + return Settings::getIdealTextColor(modInfo->getColor()); + } else if (column == COL_NAME) { + int highlight = modInfo->getHighlight(); + if (highlight & ModInfo::HIGHLIGHT_IMPORTANT) + return QBrush(Qt::darkRed); + else if (highlight & ModInfo::HIGHLIGHT_INVALID) + return QBrush(Qt::darkGray); + } else if (column == COL_VERSION) { + if (!modInfo->getNewestVersion().isValid()) { + return QVariant(); + } else if (modInfo->updateAvailable() || modInfo->downgradeAvailable()) { + return QBrush(Qt::red); + } else { + return QBrush(Qt::darkGreen); + } + } + return QVariant(); + } else if ((role == Qt::BackgroundRole) + || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) { + bool overwrite = m_Overwrite.find(modIndex) != m_Overwrite.end(); + bool archiveOverwrite = m_ArchiveOverwrite.find(modIndex) != m_ArchiveOverwrite.end(); + bool archiveLooseOverwrite = m_ArchiveLooseOverwrite.find(modIndex) != m_ArchiveLooseOverwrite.end(); + bool overwritten = m_Overwritten.find(modIndex) != m_Overwritten.end(); + bool archiveOverwritten = m_ArchiveOverwritten.find(modIndex) != m_ArchiveOverwritten.end(); + bool archiveLooseOverwritten = m_ArchiveLooseOverwritten.find(modIndex) != m_ArchiveLooseOverwritten.end(); + if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_PLUGIN) { + return Settings::instance().modlistContainsPluginColor(); + } else if (overwritten || archiveLooseOverwritten) { + return Settings::instance().modlistOverwritingLooseColor(); + } else if (overwrite || archiveLooseOverwrite) { + return Settings::instance().modlistOverwrittenLooseColor(); + } else if (archiveOverwritten) { + return Settings::instance().modlistOverwritingArchiveColor(); + } else if (archiveOverwrite) { + return Settings::instance().modlistOverwrittenArchiveColor(); + } else if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) + && modInfo->getColor().isValid() + && ((role != ViewMarkingScrollBar::DEFAULT_ROLE) + || Settings::instance().colorSeparatorScrollbar())) { + return modInfo->getColor(); + } else { + return QVariant(); + } + } else if (role == Qt::ToolTipRole) { + if (column == COL_FLAGS) { + QString result; + + for (ModInfo::EFlag flag : modInfo->getFlags()) { + if (result.length() != 0) result += "
"; + result += getFlagText(flag, modInfo); + } + + return result; + } else if (column == COL_CONTENT) { + return contentsToToolTip(modInfo->getContents()); + } else if (column == COL_NAME) { + try { + return modInfo->getDescription(); + } catch (const std::exception &e) { + qCritical("invalid mod description: %s", e.what()); + return QString(); + } + } else if (column == COL_VERSION) { + QString text = tr("installed version: \"%1\", newest version: \"%2\"").arg(modInfo->getVersion().displayString(3)).arg(modInfo->getNewestVersion().displayString(3)); + if (modInfo->downgradeAvailable()) { + text += "
" + tr("The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn " + "(i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. " + "Either way you may want to \"upgrade\"."); + } + if (modInfo->getNexusID() > 0) { + if (!modInfo->canBeUpdated()) { + text += "
" + tr("This mod was last checked on %1. It will be available to check after %2.") + .arg(modInfo->getLastNexusUpdate().toLocalTime().toString(Qt::DefaultLocaleShortDate)) + .arg(modInfo->getLastNexusUpdate().toLocalTime().addSecs(3600).time().toString(Qt::DefaultLocaleShortDate)); + } else { + text += "
" + tr("This mod is eligible for an update check."); + } + } + return text; + } else if (column == COL_CATEGORY) { + const std::set &categories = modInfo->getCategories(); + std::wostringstream categoryString; + categoryString << ToWString(tr("Categories:
")); + CategoryFactory &categoryFactory = CategoryFactory::instance(); + for (std::set::const_iterator catIter = categories.begin(); + catIter != categories.end(); ++catIter) { + if (catIter != categories.begin()) { + categoryString << " , "; + } + try { + categoryString << "" << ToWString(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*catIter))) << ""; + } catch (const std::exception &e) { + qCritical("failed to generate tooltip: %s", e.what()); + return QString(); + } + } + + return ToQString(categoryString.str()); + } else if (column == COL_NOTES) { + return getFlagText(ModInfo::FLAG_NOTES, modInfo); + } else { + return QVariant(); + } + } else { + return QVariant(); + } +} + + +bool ModList::renameMod(int index, const QString &newName) +{ + QString nameFixed = newName; + if (!fixDirectoryName(nameFixed) || nameFixed.isEmpty()) { + MessageDialog::showMessage(tr("Invalid name"), nullptr); + return false; + } + + if (ModList::allMods().contains(nameFixed, Qt::CaseInsensitive) && nameFixed.toLower()!=ModInfo::getByIndex(index)->name().toLower() ) { + MessageDialog::showMessage(tr("Name is already in use by another mod"), nullptr); + return false; + } + + ModInfo::Ptr modInfo = ModInfo::getByIndex(index); + QString oldName = modInfo->name(); + if (nameFixed != oldName) { + // before we rename, ensure there is no scheduled asynchronous to rewrite + m_Profile->cancelModlistWrite(); + + + if (modInfo->setName(nameFixed)) + // Notice there is a good chance that setName() updated the modinfo indexes + // the modRenamed() call will refresh the indexes in the current profile + // and update the modlists in all profiles + emit modRenamed(oldName, nameFixed); + } + + // invalidate the currently displayed state of this list + notifyChange(-1); + return true; +} + +bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (m_Profile == nullptr) return false; + + if (static_cast(index.row()) >= ModInfo::getNumMods()) { + return false; + } + + int modID = index.row(); + + ModInfo::Ptr info = ModInfo::getByIndex(modID); + IModList::ModStates oldState = state(modID); + + bool result = false; + + emit aboutToChangeData(); + + if (role == Qt::CheckStateRole) { + bool enabled = value.toInt() == Qt::Checked; + if (m_Profile->modEnabled(modID) != enabled) { + m_Profile->setModEnabled(modID, enabled); + m_Modified = true; + m_LastCheck.restart(); + emit modlistChanged(index, role); + } + result = true; + emit dataChanged(index, index); + } else if (role == Qt::EditRole) { + switch (index.column()) { + case COL_NAME: { + auto flags = info->getFlags(); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_SEPARATOR) != flags.end()) + { + result = renameMod(modID, value.toString() + "_separator"); + } + else + result = renameMod(modID, value.toString()); + } break; + case COL_PRIORITY: { + bool ok = false; + int newPriority = value.toInt(&ok); + if (ok && newPriority < 0) { + newPriority = 0; + } + if (ok) { + m_Profile->setModPriority(modID, newPriority); + + emit modorder_changed(); + result = true; + } else { + result = false; + } + } break; + case COL_MODID: { + bool ok = false; + int newID = value.toInt(&ok); + if (ok) { + info->setNexusID(newID); + emit modlistChanged(index, role); + result = true; + } else { + result = false; + } + } break; + case COL_VERSION: { + VersionInfo::VersionScheme scheme = info->getVersion().scheme(); + VersionInfo version(value.toString(), scheme, true); + if (version.isValid()) { + info->setVersion(version); + result = true; + } else { + result = false; + } + } break; + case COL_NOTES: { + info->setComments(value.toString()); + result = true; + } break; + default: { + qWarning("edit on column \"%s\" not supported", + getColumnName(index.column()).toUtf8().constData()); + result = false; + } break; + } + if (result) { + emit dataChanged(index, index); + } + } + + emit postDataChanged(); + + IModList::ModStates newState = state(modID); + if (oldState != newState) { + try { + m_ModStateChanged(info->name(), newState); + } catch (const std::exception &e) { + qCritical("failed to invoke state changed notification: %s", e.what()); + } catch (...) { + qCritical("failed to invoke state changed notification: unknown exception"); + } + } + + return result; +} + + +QVariant ModList::headerData(int section, Qt::Orientation orientation, + int role) const +{ + if (orientation == Qt::Horizontal) { + if (role == Qt::DisplayRole) { + return getColumnName(section); + } else if (role == Qt::ToolTipRole) { + return getColumnToolTip(section); + } else if (role == Qt::TextAlignmentRole) { + return QVariant(Qt::AlignCenter); + } else if (role == Qt::SizeHintRole) { + QSize temp = m_FontMetrics.size(Qt::TextSingleLine, getColumnName(section)); + temp.rwidth() += 20; + temp.rheight() += 12; + return temp; + } + } + return QAbstractItemModel::headerData(section, orientation, role); +} + + +Qt::ItemFlags ModList::flags(const QModelIndex &modelIndex) const +{ + Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex); + if (modelIndex.internalId() < 0) { + return Qt::ItemIsEnabled; + } + if (modelIndex.isValid()) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(modelIndex.row()); + std::vector flags = modInfo->getFlags(); + if (modInfo->getFixedPriority() == INT_MIN) { + result |= Qt::ItemIsDragEnabled; + result |= Qt::ItemIsUserCheckable; + if ((modelIndex.column() == COL_PRIORITY) || + (modelIndex.column() == COL_VERSION) || + (modelIndex.column() == COL_MODID)) { + result |= Qt::ItemIsEditable; + } + if (((modelIndex.column() == COL_NAME) || + (modelIndex.column() == COL_NOTES)) + && (std::find(flags.begin(), flags.end(), ModInfo::FLAG_FOREIGN) == flags.end())) { + result |= Qt::ItemIsEditable; + } + } + if (m_DropOnItems + && (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) == flags.end())) { + result |= Qt::ItemIsDropEnabled; + } + } else { + if (!m_DropOnItems) result |= Qt::ItemIsDropEnabled; + } + return result; +} + + +QStringList ModList::mimeTypes() const +{ + QStringList result = QAbstractItemModel::mimeTypes(); + result.append("text/uri-list"); + return result; +} + +QMimeData *ModList::mimeData(const QModelIndexList &indexes) const +{ + QMimeData *result = QAbstractItemModel::mimeData(indexes); + result->setData("text/plain", "mod"); + return result; +} + +void ModList::changeModPriority(std::vector sourceIndices, int newPriority) +{ + if (m_Profile == nullptr) return; + + emit layoutAboutToBeChanged(); + Profile *profile = m_Profile; + + // sort the moving mods by ascending priorities + std::sort(sourceIndices.begin(), sourceIndices.end(), + [profile](const int &LHS, const int &RHS) { + return profile->getModPriority(LHS) > profile->getModPriority(RHS); + }); + + // move mods that are decreasing in priority + for (std::vector::const_iterator iter = sourceIndices.begin(); + iter != sourceIndices.end(); ++iter) { + int oldPriority = profile->getModPriority(*iter); + if (oldPriority > newPriority) { + profile->setModPriority(*iter, newPriority); + m_ModMoved(ModInfo::getByIndex(*iter)->name(), oldPriority, newPriority); + } + } + + // sort the moving mods by descending priorities + std::sort(sourceIndices.begin(), sourceIndices.end(), + [profile](const int &LHS, const int &RHS) { + return profile->getModPriority(LHS) < profile->getModPriority(RHS); + }); + + // if at least one mod is increasing in priority, the target index is + // that of the row BELOW the dropped location, otherwise it's the one above + for (std::vector::const_iterator iter = sourceIndices.begin(); + iter != sourceIndices.end(); ++iter) { + int oldPriority = profile->getModPriority(*iter); + if (oldPriority < newPriority) { + --newPriority; + break; + } + } + + // move mods that are increasing in priority + for (std::vector::const_iterator iter = sourceIndices.begin(); + iter != sourceIndices.end(); ++iter) { + int oldPriority = profile->getModPriority(*iter); + if (oldPriority < newPriority) { + profile->setModPriority(*iter, newPriority); + m_ModMoved(ModInfo::getByIndex(*iter)->name(), oldPriority, newPriority); + } + } + + emit layoutChanged(); + + emit modorder_changed(); +} + + +void ModList::changeModPriority(int sourceIndex, int newPriority) +{ + if (m_Profile == nullptr) return; + emit layoutAboutToBeChanged(); + + m_Profile->setModPriority(sourceIndex, newPriority); + + emit layoutChanged(); + + emit modorder_changed(); +} + +void ModList::setOverwriteMarkers(const std::set &overwrite, const std::set &overwritten) +{ + m_Overwrite = overwrite; + m_Overwritten = overwritten; + notifyChange(0, rowCount() - 1); +} + +void ModList::setArchiveOverwriteMarkers(const std::set &overwrite, const std::set &overwritten) +{ + m_ArchiveOverwrite = overwrite; + m_ArchiveOverwritten = overwritten; + notifyChange(0, rowCount() - 1); +} + +void ModList::setArchiveLooseOverwriteMarkers(const std::set &overwrite, const std::set &overwritten) +{ + m_ArchiveLooseOverwrite = overwrite; + m_ArchiveLooseOverwritten = overwritten; + notifyChange(0, rowCount() - 1); +} + +void ModList::setPluginContainer(PluginContainer *pluginContianer) +{ + m_PluginContainer = pluginContianer; +} + +bool ModList::modInfoAboutToChange(ModInfo::Ptr info) +{ + if (m_ChangeInfo.name.isEmpty()) { + m_ChangeInfo.name = info->name(); + m_ChangeInfo.state = state(info->name()); + return true; + } else { + return false; + } +} + +void ModList::modInfoChanged(ModInfo::Ptr info) +{ + if (info->name() == m_ChangeInfo.name) { + IModList::ModStates newState = state(info->name()); + if (m_ChangeInfo.state != newState) { + m_ModStateChanged(info->name(), newState); + } + + int row = ModInfo::getIndex(info->name()); + info->testValid(); + emit aboutToChangeData(); + emit dataChanged(index(row, 0), index(row, columnCount())); + emit postDataChanged(); + } else { + qCritical("modInfoChanged not called after modInfoAboutToChange"); + } + m_ChangeInfo.name = QString(); +} + +void ModList::disconnectSlots() { + m_ModMoved.disconnect_all_slots(); + m_ModStateChanged.disconnect_all_slots(); +} + +int ModList::timeElapsedSinceLastChecked() const +{ + return m_LastCheck.elapsed(); +} + +void ModList::highlightMods(const QItemSelectionModel *selection, const MOShared::DirectoryEntry &directoryEntry) +{ + for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { + ModInfo::getByIndex(i)->setPluginSelected(false); + } + for (QModelIndex idx : selection->selectedRows(PluginList::COL_NAME)) { + QString modName = idx.data().toString(); + + const MOShared::FileEntry::Ptr fileEntry = directoryEntry.findFile(modName.toStdWString()); + if (fileEntry.get() != nullptr) { + bool archive = false; + std::vector>> origins; + { + std::vector>> alternatives = fileEntry->getAlternatives(); + origins.insert(origins.end(), std::pair>(fileEntry->getOrigin(archive), fileEntry->getArchive())); + } + for (auto originInfo : origins) { + MOShared::FilesOrigin &origin = directoryEntry.getOriginByID(originInfo.first); + for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { + if (ModInfo::getByIndex(i)->internalName() == QString::fromStdWString(origin.getName())) { + ModInfo::getByIndex(i)->setPluginSelected(true); + break; + } + } + } + } + } + notifyChange(0, rowCount() - 1); +} + +IModList::ModStates ModList::state(unsigned int modIndex) const +{ + IModList::ModStates result; + if (modIndex != UINT_MAX) { + result |= IModList::STATE_EXISTS; + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + if (modInfo->isEmpty()) { + result |= IModList::STATE_EMPTY; + } + if (modInfo->endorsedState() == ModInfo::ENDORSED_TRUE) { + result |= IModList::STATE_ENDORSED; + } + if (modInfo->isValid()) { + result |= IModList::STATE_VALID; + } + if (modInfo->isRegular()) { + QSharedPointer modInfoRegular = modInfo.staticCast(); + if (modInfoRegular->isAlternate() && !modInfoRegular->isConverted()) + result |= IModList::STATE_ALTERNATE; + if (!modInfo->isValid() && modInfoRegular->isValidated()) + result |= IModList::STATE_VALID; + } + if (modInfo->canBeEnabled()) { + if (m_Profile->modEnabled(modIndex)) { + result |= IModList::STATE_ACTIVE; + } + } else { + result |= IModList::STATE_ESSENTIAL; + } + } + return result; +} + +QString ModList::displayName(const QString &internalName) const +{ + unsigned int modIndex = ModInfo::getIndex(internalName); + if (modIndex == UINT_MAX) { + // might be better to throw an exception? + return internalName; + } else { + return ModInfo::getByIndex(modIndex)->name(); + } +} + +QStringList ModList::allMods() const +{ + QStringList result; + for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { + result.append(ModInfo::getByIndex(i)->internalName()); + } + return result; +} + +IModList::ModStates ModList::state(const QString &name) const +{ + unsigned int modIndex = ModInfo::getIndex(name); + + return state(modIndex); +} + +bool ModList::setActive(const QString &name, bool active) +{ + unsigned int modIndex = ModInfo::getIndex(name); + if (modIndex == UINT_MAX) { + return false; + } else { + m_Profile->setModEnabled(modIndex, active); + + IModList::ModStates newState = state(modIndex); + m_ModStateChanged(name, newState); + return true; + } +} + +int ModList::priority(const QString &name) const +{ + unsigned int modIndex = ModInfo::getIndex(name); + if (modIndex == UINT_MAX) { + return -1; + } else { + return m_Profile->getModPriority(modIndex); + } +} + +bool ModList::setPriority(const QString &name, int newPriority) +{ + if ((newPriority < 0) || (newPriority >= static_cast(m_Profile->numRegularMods()))) { + return false; + } + + unsigned int modIndex = ModInfo::getIndex(name); + if (modIndex == UINT_MAX) { + return false; + } else { + m_Profile->setModPriority(modIndex, newPriority); + notifyChange(modIndex); + return true; + } +} + +bool ModList::onModStateChanged(const std::function &func) +{ + auto conn = m_ModStateChanged.connect(func); + return conn.connected(); +} + +bool ModList::onModMoved(const std::function &func) +{ + auto conn = m_ModMoved.connect(func); + return conn.connected(); +} + +bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &parent) +{ + QStringList source; + QStringList target; + + if (row == -1) { + row = parent.row(); + } + ModInfo::Ptr modInfo = ModInfo::getByIndex(row); + QDir modDirectory(modInfo->absolutePath()); + QDir gameDirectory(Settings::instance().getOverwriteDirectory()); + + unsigned int overwriteIndex = ModInfo::findMod([] (ModInfo::Ptr mod) -> bool { + std::vector flags = mod->getFlags(); + return std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end(); }); + + QString overwriteName = ModInfo::getByIndex(overwriteIndex)->name(); + + for (const QUrl &url : mimeData->urls()) { + //qDebug("URL drop requested: %s", qUtf8Printable(url.toLocalFile())); + if (!url.isLocalFile()) { + qDebug("URL drop ignored: Not a local file."); + continue; + } + QString relativePath = gameDirectory.relativeFilePath(url.toLocalFile()); + if (relativePath.startsWith("..")) { + qDebug("URL drop ignored: relative path starts with .."); + continue; + } + source.append(url.toLocalFile()); + target.append(modDirectory.absoluteFilePath(relativePath)); + emit fileMoved(relativePath, overwriteName, modInfo->name()); + } + + if (source.count() != 0) { + if (!shellMove(source, target)) { + qDebug("Move failed %lu",::GetLastError()); + return false; + } + } + + if (!modInfo->isValid()) { + modInfo->testValid(); + } + + return true; +} + +bool ModList::dropMod(const QMimeData *mimeData, int row, const QModelIndex &parent) +{ + + try { + QByteArray encoded = mimeData->data("application/x-qabstractitemmodeldatalist"); + QDataStream stream(&encoded, QIODevice::ReadOnly); + std::vector sourceRows; + + while (!stream.atEnd()) { + int sourceRow, col; + QMap roleDataMap; + stream >> sourceRow >> col >> roleDataMap; + if (col == 0) { + sourceRows.push_back(sourceRow); + } + } + + if (row == -1) { + row = parent.row(); + } + + if ((row < 0) || (static_cast(row) >= ModInfo::getNumMods())) { + return false; + } + + int newPriority = 0; + { + if ((row < 0) || (row > static_cast(m_Profile->numRegularMods()))) { + newPriority = m_Profile->numRegularMods() + 1; + } else { + newPriority = m_Profile->getModPriority(row); + } + if (newPriority == -1) { + newPriority = m_Profile->numRegularMods() + 1; + } + } + changeModPriority(sourceRows, newPriority); + } catch (const std::exception &e) { + reportError(tr("drag&drop failed: %1").arg(e.what())); + } + + return false; +} + + +bool ModList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int, const QModelIndex &parent) +{ + if (action == Qt::IgnoreAction) { + return true; + } + + if (m_Profile == nullptr) return false; + + if (mimeData->hasUrls()) { + return dropURLs(mimeData, row, parent); + } else if (mimeData->hasText()) { + return dropMod(mimeData, row, parent); + } else { + return false; + } +} + +void ModList::removeRowForce(int row, const QModelIndex &parent) +{ + if (static_cast(row) >= ModInfo::getNumMods()) { + return; + } + if (m_Profile == nullptr) return; + + ModInfo::Ptr modInfo = ModInfo::getByIndex(row); + + bool wasEnabled = m_Profile->modEnabled(row); + + m_Profile->setModEnabled(row, false); + + m_Profile->cancelModlistWrite(); + beginRemoveRows(parent, row, row); + ModInfo::removeMod(row); + endRemoveRows(); + m_Profile->refreshModStatus(); // removes the mod from the status list + m_Profile->writeModlist(); // this ensures the modified list gets written back before new mods can be installed + + if (wasEnabled) { + emit removeOrigin(modInfo->name()); + } + + emit modUninstalled(modInfo->getInstallationFile()); +} + +bool ModList::removeRows(int row, int count, const QModelIndex &parent) +{ + if (static_cast(row) >= ModInfo::getNumMods()) { + return false; + } + if (m_Profile == nullptr) { + return false; + } + + bool success = false; + + if (count == 1) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(row); + std::vector flags = modInfo->getFlags(); + if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) && (QDir(modInfo->absolutePath()).count() > 2)) { + emit clearOverwrite(); + success = true; + } + } + + for (int i = 0; i < count; ++i) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(row + i); + if (!modInfo->isRegular()) { + continue; + } + + success = true; + + QMessageBox confirmBox(QMessageBox::Question, tr("Confirm"), + tr("Are you sure you want to remove \"%1\"?").arg(modInfo->name()), + QMessageBox::Yes | QMessageBox::No); + + if (confirmBox.exec() == QMessageBox::Yes) { + m_Profile->setModEnabled(row + i, false); + removeRowForce(row + i, parent); + } + } + + return success; +} + + +void ModList::notifyChange(int rowStart, int rowEnd) +{ + if (rowStart < 0) { + m_Overwrite.clear(); + m_Overwritten.clear(); + m_ArchiveOverwrite.clear(); + m_ArchiveOverwritten.clear(); + m_ArchiveLooseOverwrite.clear(); + m_ArchiveLooseOverwritten.clear(); + beginResetModel(); + endResetModel(); + } else { + if (rowEnd == -1) { + rowEnd = rowStart; + } + emit dataChanged(this->index(rowStart, 0), this->index(rowEnd, this->columnCount() - 1)); + } +} + + +QModelIndex ModList::index(int row, int column, const QModelIndex&) const +{ + if ((row < 0) || (row >= rowCount()) || (column < 0) || (column >= columnCount())) { + return QModelIndex(); + } + QModelIndex res = createIndex(row, column, row); + return res; +} + + +QModelIndex ModList::parent(const QModelIndex&) const +{ + return QModelIndex(); +} + +QMap ModList::itemData(const QModelIndex &index) const +{ + QMap result = QAbstractItemModel::itemData(index); + result[Qt::UserRole] = data(index, Qt::UserRole); + return result; +} + + +void ModList::dropModeUpdate(bool dropOnItems) +{ + if (m_DropOnItems != dropOnItems) { + m_DropOnItems = dropOnItems; + } +} + + +QString ModList::getColumnName(int column) +{ + switch (column) { + case COL_FLAGS: return tr("Flags"); + case COL_CONTENT: return tr("Content"); + case COL_NAME: return tr("Mod Name"); + case COL_VERSION: return tr("Version"); + case COL_PRIORITY: return tr("Priority"); + case COL_CATEGORY: return tr("Category"); + case COL_GAME: return tr("Source Game"); + case COL_MODID: return tr("Nexus ID"); + case COL_INSTALLTIME: return tr("Installation"); + case COL_NOTES: return tr("Notes"); + default: return tr("unknown"); + } +} + + +QString ModList::getColumnToolTip(int column) +{ + switch (column) { + case COL_NAME: return tr("Name of your mods"); + case COL_VERSION: return tr("Version of the mod (if available)"); + case COL_PRIORITY: return tr("Installation priority of your mod. The higher, the more \"important\" it is and thus " + "overwrites files from mods with lower priority."); + case COL_CATEGORY: return tr("Category of the mod."); + case COL_GAME: return tr("The source game which was the origin of this mod."); + case COL_MODID: return tr("Id of the mod as used on Nexus."); + case COL_FLAGS: return tr("Emblemes to highlight things that might require attention."); + case COL_CONTENT: return tr("Depicts the content of the mod:
" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + + "
Game plugins (esp/esm/esl)
Interface
Meshes
BSA
Textures
Sounds
Music
Strings
Scripts (Papyrus)
Script Extender plugins
SkyProc Patcher
Mod Configuration Menu
INI files
ModGroup files
"); + case COL_INSTALLTIME: return tr("Time this mod was installed"); + case COL_NOTES: return tr("User notes about the mod"); + default: return tr("unknown"); + } +} + + +bool ModList::moveSelection(QAbstractItemView *itemView, int direction) +{ + QItemSelectionModel *selectionModel = itemView->selectionModel(); + + const QAbstractProxyModel *proxyModel = qobject_cast(selectionModel->model()); + const QSortFilterProxyModel *filterModel = nullptr; + + while ((filterModel == nullptr) && (proxyModel != nullptr)) { + filterModel = qobject_cast(proxyModel); + if (filterModel == nullptr) { + proxyModel = qobject_cast(proxyModel->sourceModel()); + } + } + if (filterModel == nullptr) { + return true; + } + + int offset = -1; + if (((direction < 0) && (filterModel->sortOrder() == Qt::DescendingOrder)) || + ((direction > 0) && (filterModel->sortOrder() == Qt::AscendingOrder))) { + offset = 1; + } + + QModelIndexList rows = selectionModel->selectedRows(); + if (direction > 0) { + for (int i = 0; i < rows.size() / 2; ++i) { + rows.swap(i, rows.size() - i - 1); + } + } + for (QModelIndex idx : rows) { + if (filterModel != nullptr) { + idx = filterModel->mapToSource(idx); + } + int newPriority = m_Profile->getModPriority(idx.row()) + offset; + if ((newPriority >= 0) && (newPriority < static_cast(m_Profile->numRegularMods()))) { + m_Profile->setModPriority(idx.row(), newPriority); + notifyChange(idx.row()); + } + } + emit modorder_changed(); + return true; +} + +bool ModList::deleteSelection(QAbstractItemView *itemView) +{ + QItemSelectionModel *selectionModel = itemView->selectionModel(); + + QModelIndexList rows = selectionModel->selectedRows(); + if (rows.count() > 1) { + emit removeSelectedMods(); + } else if (rows.count() == 1) { + removeRow(rows[0].data(Qt::UserRole + 1).toInt(), QModelIndex()); + } + return true; +} + +bool ModList::toggleSelection(QAbstractItemView *itemView) +{ + emit aboutToChangeData(); + + QItemSelectionModel *selectionModel = itemView->selectionModel(); + + QList modsToEnable; + QList modsToDisable; + QModelIndexList dirtyMods; + for (QModelIndex idx : selectionModel->selectedRows()) { + int modId = idx.data(Qt::UserRole + 1).toInt(); + if (m_Profile->modEnabled(modId)) { + modsToDisable.append(modId); + dirtyMods.append(idx); + } else { + modsToEnable.append(modId); + dirtyMods.append(idx); + } + } + + m_Profile->setModsEnabled(modsToEnable, modsToDisable); + + emit modlistChanged(dirtyMods, 0); + + m_Modified = true; + m_LastCheck.restart(); + + emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1)); + + emit postDataChanged(); + + return true; +} + +bool ModList::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::ContextMenu) { + QContextMenuEvent *contextEvent = static_cast(event); + QWidget *object = qobject_cast(obj); + if ((object != nullptr) && (contextEvent->reason() == QContextMenuEvent::Mouse)) { + emit requestColumnSelect(object->mapToGlobal(contextEvent->pos())); + + return true; + } + } else if ((event->type() == QEvent::KeyPress) && (m_Profile != nullptr)) { + QAbstractItemView *itemView = qobject_cast(obj); + QKeyEvent *keyEvent = static_cast(event); + + if ((itemView != nullptr) + && (keyEvent->modifiers() == Qt::ControlModifier) + && ((keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down))) { + return moveSelection(itemView, keyEvent->key() == Qt::Key_Up ? -1 : 1); + } else if (keyEvent->key() == Qt::Key_Delete) { + return deleteSelection(itemView); + } else if (keyEvent->key() == Qt::Key_Space) { + return toggleSelection(itemView); + } + return QAbstractItemModel::eventFilter(obj, event); + } + return QAbstractItemModel::eventFilter(obj, event); +} + +//note: caller needs to make sure sort proxy is updated +void ModList::enableSelected(const QItemSelectionModel *selectionModel) +{ + if (selectionModel->hasSelection()) { + QList modsToEnable; + for (auto row : selectionModel->selectedRows(COL_PRIORITY)) { + int modID = m_Profile->modIndexByPriority(row.data().toInt()); + modsToEnable.append(modID); + } + m_Profile->setModsEnabled(modsToEnable, QList()); + } +} + +//note: caller needs to make sure sort proxy is updated +void ModList::disableSelected(const QItemSelectionModel *selectionModel) +{ + if (selectionModel->hasSelection()) { + QList modsToDisable; + for (auto row : selectionModel->selectedRows(COL_PRIORITY)) { + int modID = m_Profile->modIndexByPriority(row.data().toInt()); + modsToDisable.append(modID); + } + m_Profile->setModsEnabled(QList(), modsToDisable); + } +} -- cgit v1.3.1 From 6030a1d1fcceae8bd4063362e9c1dbe684a4732b Mon Sep 17 00:00:00 2001 From: Silarn Date: Tue, 29 Jan 2019 20:29:23 -0600 Subject: Sorting logic fix and show last updated date --- src/modinfo.cpp | 2 +- src/modlist.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src/modinfo.cpp') diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 5d5cb57e..e9cfd77b 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -326,7 +326,7 @@ int ModInfo::autoUpdateCheck(PluginContainer *pluginContainer, QObject *receiver } std::sort(sortedMods.begin(), sortedMods.end(), [](QSharedPointer a, QSharedPointer b) -> bool { - return a->getLastNexusUpdate() > b->getLastNexusUpdate(); + return a->getLastNexusUpdate() < b->getLastNexusUpdate(); }); if (sortedMods.size() > 10) diff --git a/src/modlist.cpp b/src/modlist.cpp index 1ec5b556..8c502f99 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -465,6 +465,7 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const .arg(modInfo->getLastNexusUpdate().toLocalTime().addSecs(3600).time().toString(Qt::DefaultLocaleShortDate)); } else { text += "
" + tr("This mod is eligible for an update check."); + text += "
" + tr("It was last checked on %1").arg(modInfo->getLastNexusUpdate().toLocalTime().toString(Qt::DefaultLocaleShortDate)); } } return text; -- cgit v1.3.1 From 2b5862ef0144d49e1927b5c914cbcea7183fab8a Mon Sep 17 00:00:00 2001 From: Silarn Date: Fri, 1 Feb 2019 17:45:05 -0600 Subject: Multiple updates: * Remove periodic auto-update * Add context menu force-update for mod list * Fix some bugs with update parsing - Ignore update files that are deleted - Fix remaining issue with failing to get all mods due to capitalization * Remove queue retry when out of requests * Clear all requests when out of requests * More potential improvements to login/validation process --- src/mainwindow.cpp | 54 +++++++++++++++++++++++++++++------------------- src/mainwindow.h | 4 ++-- src/modinfo.cpp | 42 ++++++++++++++++++------------------- src/modinfo.h | 2 +- src/nexusinterface.cpp | 30 ++++++++++----------------- src/nexusinterface.h | 2 -- src/nxmaccessmanager.cpp | 4 ++++ 7 files changed, 71 insertions(+), 67 deletions(-) (limited to 'src/modinfo.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 3a341298..fecf8704 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -439,10 +439,6 @@ MainWindow::MainWindow(QSettings &initSettings connect(&m_SaveMetaTimer, SIGNAL(timeout()), this, SLOT(saveModMetas())); m_SaveMetaTimer.start(5000); - m_ModUpdateTimer.setSingleShot(false); - connect(&m_ModUpdateTimer, SIGNAL(timeout()), this, SLOT(modUpdateCheck())); - m_ModUpdateTimer.start(300 * 1000); - setCategoryListVisible(initSettings.value("categorylist_visible", true).toBool()); FileDialogMemory::restore(initSettings); @@ -495,8 +491,6 @@ MainWindow::MainWindow(QSettings &initSettings updatePluginCount(); updateModCount(); - - modUpdateCheck(); } @@ -4056,6 +4050,22 @@ void MainWindow::ignoreUpdate() { } } +void MainWindow::checkModUpdates_clicked() +{ + std::multimap IDs; + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + IDs.insert(std::make_pair(info->getGameName(), info->getNexusID())); + } + } else { + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + IDs.insert(std::make_pair(info->getGameName(), info->getNexusID())); + } + modUpdateCheck(IDs); +} + void MainWindow::unignoreUpdate() { QItemSelectionModel *selection = ui->modList->selectionModel(); @@ -4411,7 +4421,7 @@ void MainWindow::initModListContextMenu(QMenu *menu) menu->addAction(tr("Enable all visible"), this, SLOT(enableVisibleMods())); menu->addAction(tr("Disable all visible"), this, SLOT(disableVisibleMods())); - menu->addAction(tr("Force update check"), this, SLOT(checkModsForUpdates())); + menu->addAction(tr("Check for updates"), this, SLOT(checkModsForUpdates())); menu->addAction(tr("Refresh"), &m_OrganizerCore, SLOT(profileRefresh())); menu->addAction(tr("Export to csv..."), this, SLOT(exportModListCSV())); } @@ -4518,10 +4528,11 @@ void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) menu.addAction(tr("Change versioning scheme"), this, SLOT(changeVersioningScheme())); } + if (info->getNexusID() > 0) + menu->addAction(tr("Force-check updates"), this, SLOT(checkModUpdates_clicked())); if (info->updateIgnored()) { menu.addAction(tr("Un-ignore update"), this, SLOT(unignoreUpdate())); - } - else { + } else { if (info->updateAvailable() || info->downgradeAvailable()) { menu.addAction(tr("Ignore update"), this, SLOT(ignoreUpdate())); } @@ -5447,15 +5458,15 @@ void MainWindow::modDetailsUpdated(bool) } } -void MainWindow::modUpdateCheck() +void MainWindow::modUpdateCheck(std::multimap IDs) { if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { - m_ModsToUpdate += ModInfo::autoUpdateCheck(&m_PluginContainer, this); + m_ModsToUpdate += ModInfo::manualUpdateCheck(&m_PluginContainer, this, IDs); m_RefreshProgress->setRange(0, m_ModsToUpdate); } else { QString apiKey; if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { - m_OrganizerCore.doAfterLogin([this]() { this->modUpdateCheck(); }); + m_OrganizerCore.doAfterLogin([=]() { this->modUpdateCheck(IDs); }); NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); } else { qWarning("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus."); @@ -5506,19 +5517,20 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD if (currentUpdate == updateScanData["old_file_id"].toInt()) { currentUpdate = updateScanData["new_file_id"].toInt(); finalUpdate = false; + // Apply the version data from the latest file + for (auto file : files) { + QVariantMap fileData = file.toMap(); + if (fileData["file_id"].toInt() == currentUpdate) { + if (fileData["category_id"].toInt() != 6) { + mod->setNewestVersion(fileData["version"].toString()); + foundUpdate = true; + } + } + } break; } } } - // Apply the version data from the latest file - for (auto file : files) { - QVariantMap fileData = file.toMap(); - if (fileData["file_id"].toInt() == currentUpdate) { - mod->setNewestVersion(fileData["version"].toString()); - foundUpdate = true; - } - } - break; } else if (installedFile == updateData["new_file_name"].toString()) { // This is a safety mechanism if this is the latest update file so we don't use the mod version diff --git a/src/mainwindow.h b/src/mainwindow.h index 3f3a5457..59be9800 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -348,7 +348,6 @@ private: QTimer m_CheckBSATimer; QTimer m_SaveMetaTimer; QTimer m_UpdateProblemsTimer; - QTimer m_ModUpdateTimer; QFuture m_MetaSave; @@ -509,7 +508,7 @@ private slots: void modInstalled(const QString &modName); - void modUpdateCheck(); + void modUpdateCheck(std::multimap IDs); void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); @@ -590,6 +589,7 @@ private slots: void overwriteClosed(int); void changeVersioningScheme(); + void checkModUpdates_clicked(); void ignoreUpdate(); void unignoreUpdate(); diff --git a/src/modinfo.cpp b/src/modinfo.cpp index e9cfd77b..a5b240c8 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -148,8 +148,7 @@ std::vector ModInfo::getByModID(QString game, int modID) for (auto iter : s_ModsByModID) { if (iter.first.second == modID) { if (iter.first.first.compare(game, Qt::CaseInsensitive) == 0) { - match = iter.second; - break; + match.insert(match.end(), iter.second.begin(), iter.second.end()); } } } @@ -303,6 +302,9 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv } result = organizedGames.size(); + + if (organizedGames.empty()) + qWarning("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests."); for (auto game : organizedGames) { NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString()); @@ -312,32 +314,28 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv } -int ModInfo::autoUpdateCheck(PluginContainer *pluginContainer, QObject *receiver) +int ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap IDs) { - qInfo("Initializing periodic update check."); - int result = 0; - - std::vector> sortedMods; + std::vector> mods; std::multimap organizedGames; - for (auto mod : s_Collection) { - if (mod->canBeUpdated()) { - sortedMods.push_back(mod); - } + + for (auto ID : IDs) { + auto matchedMods = getByModID(ID.first, ID.second); + mods.insert(mods.end(), matchedMods.begin(), matchedMods.end()); } + mods.erase( + std::remove_if(mods.begin(), mods.end(), [](ModInfo::Ptr mod) -> bool { return !mod->canBeUpdated(); }), + mods.end() + ); - std::sort(sortedMods.begin(), sortedMods.end(), [](QSharedPointer a, QSharedPointer b) -> bool { + std::sort(mods.begin(), mods.end(), [](QSharedPointer a, QSharedPointer b) -> bool { return a->getLastNexusUpdate() < b->getLastNexusUpdate(); }); - if (sortedMods.size() > 10) - sortedMods.resize(10); - - result = sortedMods.size(); + if (mods.size()) { + qInfo("Checking updates for %d mods...", mods.size()); - if (sortedMods.size()) { - qInfo("Checking updates for %d mods...", sortedMods.size()); - - for (auto mod : sortedMods) { + for (auto mod : mods) { organizedGames.insert(std::make_pair(mod->getGameName(), mod->getNexusID())); } @@ -345,10 +343,10 @@ int ModInfo::autoUpdateCheck(PluginContainer *pluginContainer, QObject *receiver NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString()); } } else { - qInfo("No mods require updates at this time."); + qInfo("None of the selected mods can be updated."); } - return result; + return organizedGames.size(); } diff --git a/src/modinfo.h b/src/modinfo.h index ae9dd1f3..9343a98c 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -188,7 +188,7 @@ public: /** * @brief run a limited batch of mod update checks for "newest version" information */ - static int autoUpdateCheck(PluginContainer *pluginContainer, QObject *receiver); + static int manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap IDs); /** * @brief query nexus information for every mod and update the "newest version" information diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 1d6d2015..130a87b1 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -155,10 +155,6 @@ NexusInterface::NexusInterface(PluginContainer *pluginContainer) m_AccessManager = new NXMAccessManager(this, m_MOVersion.displayString(3)); m_DiskCache = new QNetworkDiskCache(this); connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString))); - - m_RetryTimer.setSingleShot(true); - m_RetryTimer.setInterval(2000); - m_RetryTimer.callOnTimeout(this, &NexusInterface::nextRequest); } NXMAccessManager *NexusInterface::getAccessManager() @@ -468,8 +464,6 @@ IPluginGame* NexusInterface::getGame(QString gameName) const void NexusInterface::cleanup() { -// delete m_AccessManager; -// delete m_DiskCache; m_AccessManager = nullptr; m_DiskCache = nullptr; } @@ -487,19 +481,6 @@ void NexusInterface::nextRequest() return; } - if (m_RemainingDailyRequests + m_RemainingHourlyRequests <= 0) { - if (!m_RetryTimer.isActive()) { - QTime time = QTime::currentTime(); - QTime targetTime; - targetTime.setHMS((time.hour() + 1) % 23, 0, 5); - m_RetryTimer.start(time.msecsTo(targetTime)); - QString warning("You've exceeded the Nexus API rate limit and requests are now being throttled. " - "Your next batch of requests will be available in approximately %1 minutes and %2 seconds."); - qWarning() << warning.arg(time.secsTo(targetTime) / 60).arg(time.secsTo(targetTime) % 60); - } - return; - } - if (requiresLogin(m_RequestQueue.head()) && !getAccessManager()->validated()) { if (!getAccessManager()->validateAttempted()) { emit needLogin(); @@ -509,6 +490,17 @@ void NexusInterface::nextRequest() } } + if (m_RemainingDailyRequests + m_RemainingHourlyRequests <= 0) { + m_RequestQueue.clear(); + QTime time = QTime::currentTime(); + QTime targetTime; + targetTime.setHMS((time.hour() + 1) % 23, 5, 0); + QString warning("You've exceeded the Nexus API rate limit and requests are now being throttled. " + "Your next batch of requests will be available in approximately %1 minutes and %2 seconds."); + qWarning() << warning.arg(time.secsTo(targetTime) / 60).arg(time.secsTo(targetTime) % 60); + return; + } + NXMRequestInfo info = m_RequestQueue.dequeue(); info.m_Timeout = new QTimer(this); info.m_Timeout->setInterval(60000); diff --git a/src/nexusinterface.h b/src/nexusinterface.h index 64f15961..db4ebfd6 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -460,8 +460,6 @@ private: PluginContainer *m_PluginContainer; - QTimer m_RetryTimer; - int m_RemainingDailyRequests; int m_RemainingHourlyRequests; int m_MaxDailyRequests; diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index d1720c76..7ab474a7 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -268,9 +268,13 @@ void NXMAccessManager::validateFinished() m_ValidateState = VALIDATE_VALID; emit validateSuccessful(true); } else { + m_ApiKey.clear(); + m_ValidateState = VALIDATE_NOT_VALID; emit validateFailed(tr("Validation failed, please reauthenticate in the Settings -> Nexus tab: %1").arg(credentialsData.value("message").toString())); } } else { + m_ApiKey.clear(); + m_ValidateState = VALIDATE_NOT_CHECKED; emit validateFailed(tr("unknown error")); } } -- cgit v1.3.1 From 34a374fce8b96c064f2c3e020e3f86167365eef5 Mon Sep 17 00:00:00 2001 From: Silarn Date: Sat, 2 Feb 2019 13:18:36 -0600 Subject: Several api improvements: * Don't check time for force updates * Implement API restrictions for <200 remaining requests * Fix bug causing duplicate mod checks for differing short name capitalization --- src/modinfo.cpp | 6 ++--- src/nexusinterface.cpp | 67 ++++++++++++++++++++++++++++++-------------------- 2 files changed, 44 insertions(+), 29 deletions(-) (limited to 'src/modinfo.cpp') diff --git a/src/modinfo.cpp b/src/modinfo.cpp index a5b240c8..555dcb68 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -297,7 +297,7 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv std::multimap organizedGames; for (auto mod : s_Collection) { if (mod->canBeUpdated()) { - organizedGames.insert(std::make_pair(mod->getGameName(), mod->getNexusID())); + organizedGames.insert(std::make_pair(mod->getGameName().toLower(), mod->getNexusID())); } } @@ -324,7 +324,7 @@ int ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiv mods.insert(mods.end(), matchedMods.begin(), matchedMods.end()); } mods.erase( - std::remove_if(mods.begin(), mods.end(), [](ModInfo::Ptr mod) -> bool { return !mod->canBeUpdated(); }), + std::remove_if(mods.begin(), mods.end(), [](ModInfo::Ptr mod) -> bool { return mod->getNexusID() <= 0; }), mods.end() ); @@ -336,7 +336,7 @@ int ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiv qInfo("Checking updates for %d mods...", mods.size()); for (auto mod : mods) { - organizedGames.insert(std::make_pair(mod->getGameName(), mod->getNexusID())); + organizedGames.insert(std::make_pair(mod->getGameName().toLower(), mod->getNexusID())); } for (auto game : organizedGames) { diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index fee8a427..55201c2b 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -325,35 +325,45 @@ int NexusInterface::requestDescription(QString gameName, int modID, QObject *rec int NexusInterface::requestModInfo(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game) { - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_MODINFO, userData, subModule, game); - m_RequestQueue.enqueue(requestInfo); + if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) { + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_MODINFO, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); - connect(this, SIGNAL(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), - receiver, SLOT(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); - nextRequest(); - return requestInfo.m_ID; + nextRequest(); + return requestInfo.m_ID; + } + qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.") + .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests)); + return -1; } int NexusInterface::requestUpdates(const int &modID, QObject *receiver, QVariant userData, QString gameName, const QString &subModule) { - IPluginGame *game = getGame(gameName); - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, game); - m_RequestQueue.enqueue(requestInfo); + if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) { + IPluginGame *game = getGame(gameName); + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); - connect(this, SIGNAL(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), - receiver, SLOT(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); - nextRequest(); - return requestInfo.m_ID; + nextRequest(); + return requestInfo.m_ID; + } + qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.") + .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests)); + return -1; } @@ -429,18 +439,23 @@ int NexusInterface::requestDownloadURL(QString gameName, int modID, int fileID, int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game) { - NXMRequestInfo requestInfo(modID, modVersion, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game); - requestInfo.m_Endorse = endorse; - m_RequestQueue.enqueue(requestInfo); + if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) { + NXMRequestInfo requestInfo(modID, modVersion, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game); + requestInfo.m_Endorse = endorse; + m_RequestQueue.enqueue(requestInfo); - connect(this, SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), - receiver, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), + receiver, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection); - connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), - receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); - nextRequest(); - return requestInfo.m_ID; + nextRequest(); + return requestInfo.m_ID; + } + qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.") + .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests)); + return -1; } bool NexusInterface::requiresLogin(const NXMRequestInfo &info) -- cgit v1.3.1 From 177f5c0446ab09f4010c86e9d8e1d3d02a8a76a9 Mon Sep 17 00:00:00 2001 From: Silarn Date: Mon, 4 Feb 2019 00:59:05 -0600 Subject: Fix issues with duplicate requests, expand parallel requests to 6 --- src/mainwindow.cpp | 10 ++++++++-- src/modinfo.cpp | 4 ++-- src/modinforegular.cpp | 5 +++-- src/nexusinterface.h | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) (limited to 'src/modinfo.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index fecf8704..cdf363cc 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5489,6 +5489,7 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD } std::vector modsList = ModInfo::getByModID(gameNameReal, modID); + bool requiresInfo = false; for (auto mod : modsList) { bool foundUpdate = false; bool oldFile = false; @@ -5544,13 +5545,18 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD if (foundUpdate) { // Just get the standard data updates for endorsements and descriptions mod->setLastNexusUpdate(QDateTime::currentDateTimeUtc()); - mod->updateNXMInfo(); } else { // Scrape mod data here so we can use the mod version if no file update was located - NexusInterface::instance(&m_PluginContainer)->requestModInfo(gameName, modID, this, QVariant(), QString()); + requiresInfo = true; } + + if (mod->getLastNexusQuery().addDays(1) <= QDateTime::currentDateTime()) + requiresInfo = true; } + if (requiresInfo) + NexusInterface::instance(&m_PluginContainer)->requestModInfo(gameName, modID, this, QVariant(), QString()); + if (--m_ModsToUpdate <= 0) { statusBar()->hide(); } else { diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 555dcb68..a1059e24 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -294,7 +294,7 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv // NexusInterface::instance(pluginContainer)->requestUpdates(game->nexusModOrganizerID(), receiver, QVariant(), game->gameShortName(), QString()); //} - std::multimap organizedGames; + std::set> organizedGames; for (auto mod : s_Collection) { if (mod->canBeUpdated()) { organizedGames.insert(std::make_pair(mod->getGameName().toLower(), mod->getNexusID())); @@ -317,7 +317,7 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv int ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap IDs) { std::vector> mods; - std::multimap organizedGames; + std::set> organizedGames; for (auto ID : IDs) { auto matchedMods = getByModID(ID.first, ID.second); diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 89541a70..8651f15e 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -225,9 +225,9 @@ void ModInfoRegular::nxmDescriptionAvailable(QString, int, QVariant, QVariant re if ((m_EndorsedState != ENDORSED_NEVER) && (result.contains("endorsement"))) { QVariantMap endorsement = result["endorsement"].toMap(); QString endorsementStatus = endorsement["endorse_status"].toString(); - if (endorsementStatus.compare("Endorsed") == 00) + if (endorsementStatus.compare("Endorsed", Qt::CaseInsensitive) == 00) setEndorsedState(ENDORSED_TRUE); - else if (endorsementStatus.compare("Abstained") == 00) + else if (endorsementStatus.compare("Abstained", Qt::CaseInsensitive) == 00) setEndorsedState(ENDORSED_NEVER); else setEndorsedState(ENDORSED_FALSE); @@ -235,6 +235,7 @@ void ModInfoRegular::nxmDescriptionAvailable(QString, int, QVariant, QVariant re m_LastNexusQuery = QDateTime::currentDateTimeUtc(); m_MetaInfoChanged = true; saveMeta(); + disconnect(sender(), SIGNAL(nxmDescriptionAvailable(QString, int, QVariant, QVariant))); emit modDetailsUpdated(true); } diff --git a/src/nexusinterface.h b/src/nexusinterface.h index db4ebfd6..1c283335 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -435,7 +435,7 @@ private: static QAtomicInt s_NextID; }; - static const int MAX_ACTIVE_DOWNLOADS = 2; + static const int MAX_ACTIVE_DOWNLOADS = 6; private: -- cgit v1.3.1 From 5a7d0243c69d5194ee1a4d0be05d2bcaec18aeac Mon Sep 17 00:00:00 2001 From: Silarn Date: Sat, 9 Feb 2019 04:52:44 -0600 Subject: Implement bulk update check layer --- src/mainwindow.cpp | 33 +++++++++++++++++++++-- src/mainwindow.h | 1 + src/modinfo.cpp | 73 ++++++++++++++++++++++++++++++++++++++++++++------ src/modinfo.h | 2 ++ src/modinforegular.cpp | 4 +-- src/nexusinterface.cpp | 64 +++++++++++++++++++++++++++++++++++++++++++ src/nexusinterface.h | 24 ++++++++++++++++- 7 files changed, 188 insertions(+), 13 deletions(-) (limited to 'src/modinfo.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index a17b51d5..5e97a959 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5468,10 +5468,39 @@ void MainWindow::modUpdateCheck(std::multimap IDs) if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { m_OrganizerCore.doAfterLogin([=]() { this->modUpdateCheck(IDs); }); NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); - } else { + } else qWarning("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus."); + } +} + +void MainWindow::nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int) +{ + QString gameNameReal; + for (IPluginGame *game : m_PluginContainer.plugins()) { + if (game->gameNexusName() == gameName) { + gameNameReal = game->gameShortName(); + break; + } + } + QVariantList resultList = resultData.toList(); + std::set> finalMods = ModInfo::filteredMods(gameNameReal, resultList, userData.toBool(), true); + + if (finalMods.empty()) { + qInfo("None of your mods appear to have had recent file updates."); + } + + std::set> organizedGames; + for (auto mod : finalMods) { + if (mod->canBeUpdated()) { + organizedGames.insert(std::make_pair(mod->getGameName().toLower(), mod->getNexusID())); } } + + if (!finalMods.empty() && organizedGames.empty()) + qWarning("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests."); + + for (auto game : organizedGames) + NexusInterface::instance(&m_PluginContainer)->requestUpdates(game.second, this, QVariant(), game.first, QString()); } void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) @@ -5594,7 +5623,7 @@ void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userD mod->setIsEndorsed(false); } mod->setLastNexusQuery(QDateTime::currentDateTimeUtc()); - mod->setNexusLastModified(QDateTime::fromSecsSinceEpoch(result["updated_timestamp"].toInt())); + mod->setNexusLastModified(QDateTime::fromSecsSinceEpoch(result["updated_timestamp"].toInt(), Qt::UTC)); mod->saveMeta(); } } diff --git a/src/mainwindow.h b/src/mainwindow.h index 59be9800..d2704b72 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -510,6 +510,7 @@ private slots: void modUpdateCheck(std::multimap IDs); + void nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int requestID); void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int); diff --git a/src/modinfo.cpp b/src/modinfo.cpp index a1059e24..3e694baf 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -294,25 +294,82 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv // NexusInterface::instance(pluginContainer)->requestUpdates(game->nexusModOrganizerID(), receiver, QVariant(), game->gameShortName(), QString()); //} - std::set> organizedGames; + QDateTime earliest = QDateTime::currentDateTimeUtc(); + QDateTime latest; + std::set games; for (auto mod : s_Collection) { if (mod->canBeUpdated()) { - organizedGames.insert(std::make_pair(mod->getGameName().toLower(), mod->getNexusID())); + if (mod->getLastNexusUpdate() < earliest) + earliest = mod->getLastNexusUpdate(); + if (mod->getLastNexusUpdate() > latest) + latest = mod->getLastNexusUpdate(); + games.insert(mod->getGameName().toLower()); } } - result = organizedGames.size(); - - if (organizedGames.empty()) - qWarning("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests."); + if (latest < QDateTime::currentDateTimeUtc().addDays(-30)) { + std::set> organizedGames; + for (auto mod : s_Collection) { + if (mod->canBeUpdated()) { + organizedGames.insert(std::make_pair(mod->getGameName().toLower(), mod->getNexusID())); + } + } + + result = organizedGames.size(); + + if (organizedGames.empty()) + qWarning("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests."); - for (auto game : organizedGames) { - NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString()); + for (auto game : organizedGames) { + NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString()); + } + } else if (earliest < QDateTime::currentDateTimeUtc().addDays(-30)) { + for (auto gameName : games) + NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::MONTH, receiver, QVariant(true), QString()); + } else if (earliest < QDateTime::currentDateTimeUtc().addDays(-7)) { + for (auto gameName : games) + NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::MONTH, receiver, QVariant(false), QString()); + } else if (earliest < QDateTime::currentDateTimeUtc().addDays(-1)) { + for (auto gameName : games) + NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::WEEK, receiver, QVariant(false), QString()); + } else { + for (auto gameName : games) + NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::DAY, receiver, QVariant(false), QString()); } return result; } +std::set> ModInfo::filteredMods(QString gameName, QVariantList updateData, bool addOldMods, bool markUpdated) +{ + std::set> finalMods; + for (QVariant result : updateData) { + QVariantMap update = result.toMap(); + for (auto mod : s_Collection) + if (mod->getNexusID() == update["mod_id"].toInt() && mod->getGameName().compare(gameName, Qt::CaseInsensitive) == 0) + if (mod->getLastNexusUpdate() > QDateTime::fromSecsSinceEpoch(update["latest_file_update"].toInt(), Qt::UTC)) + finalMods.insert(mod); + } + + if (addOldMods) + for (auto mod : s_Collection) + if (mod->getLastNexusUpdate() < QDateTime::currentDateTimeUtc().addDays(-30) && mod->getGameName().compare(gameName, Qt::CaseInsensitive) == 0) + finalMods.insert(mod); + + if (markUpdated) { + std::set> updates; + for (auto mod : s_Collection) + if (mod->getGameName().compare(gameName, Qt::CaseInsensitive) == 0 && mod->canBeUpdated()) + updates.insert(mod); + std::set> diff; + std::set_difference(updates.begin(), updates.end(), finalMods.begin(), finalMods.end(), std::inserter(diff, diff.end())); + for (auto skipped : diff) { + skipped->setLastNexusUpdate(QDateTime::currentDateTimeUtc()); + skipped->updateNXMInfo(); + } + } + return finalMods; +} int ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap IDs) { diff --git a/src/modinfo.h b/src/modinfo.h index 348583d2..8c234225 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -195,6 +195,8 @@ public: **/ static int checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver); + static std::set> filteredMods(QString gameName, QVariantList updateData, bool addOldMods = false, bool markUpdated = false); + /** * @brief create a new mod from the specified directory and add it to the collection * @param dir directory to create from diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 10a1317c..9532bf6d 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -235,10 +235,10 @@ void ModInfoRegular::nxmDescriptionAvailable(QString, int, QVariant, QVariant re setEndorsedState(ENDORSED_FALSE); } m_LastNexusQuery = QDateTime::currentDateTimeUtc(); - m_NexusLastModified = QDateTime::fromSecsSinceEpoch(result["updated_timestamp"].toInt()); + m_NexusLastModified = QDateTime::fromSecsSinceEpoch(result["updated_timestamp"].toInt(), Qt::UTC); m_MetaInfoChanged = true; saveMeta(); - disconnect(sender(), SIGNAL(nxmDescriptionAvailable(QString, int, QVariant, QVariant))); + disconnect(sender(), SIGNAL(descriptionAvailable(QString, int, QVariant, QVariant))); emit modDetailsUpdated(true); } diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index f1fa252a..abddbd3e 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -343,6 +343,26 @@ int NexusInterface::requestModInfo(QString gameName, int modID, QObject *receive return -1; } +int NexusInterface::requestUpdateInfo(QString gameName, NexusInterface::UpdatePeriod period, QObject *receiver, QVariant userData, + const QString &subModule, const MOBase::IPluginGame const *game) +{ + if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) { + NXMRequestInfo requestInfo(period, NXMRequestInfo::TYPE_CHECKUPDATES, userData, subModule, game); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmUpdateInfoAvailable(QString, QVariant, QVariant, int)), + receiver, SLOT(nxmUpdateInfoAvailable(QString, QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; + } + qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.") + .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests)); + return -1; +} int NexusInterface::requestUpdates(const int &modID, QObject *receiver, QVariant userData, QString gameName, const QString &subModule) @@ -531,6 +551,21 @@ void NexusInterface::nextRequest() case NXMRequestInfo::TYPE_MODINFO: { url = QString("%1/games/%2/mods/%3").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); } break; + case NXMRequestInfo::TYPE_CHECKUPDATES: { + QString period; + switch (info.m_UpdatePeriod) { + case UpdatePeriod::DAY: + period = "1d"; + break; + case UpdatePeriod::WEEK: + period = "1w"; + break; + case UpdatePeriod::MONTH: + period = "1m"; + break; + } + url = QString("%1/games/%2/mods/updated?period=%3").arg(info.m_URL).arg(info.m_GameName).arg(period); + } break; case NXMRequestInfo::TYPE_FILES: case NXMRequestInfo::TYPE_GETUPDATES: { url = QString("%1/games/%2/mods/%3/files").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID); @@ -624,6 +659,9 @@ void NexusInterface::requestFinished(std::list::iterator iter) case NXMRequestInfo::TYPE_MODINFO: { emit nxmModInfoAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); } break; + case NXMRequestInfo::TYPE_CHECKUPDATES: { + emit nxmUpdateInfoAvailable(iter->m_GameName, iter->m_UserData, result, iter->m_ID); + } break; case NXMRequestInfo::TYPE_FILES: { emit nxmFilesAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); } break; @@ -726,6 +764,7 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_FileID(0) , m_Reply(nullptr) , m_Type(type) + , m_UpdatePeriod(UpdatePeriod::NONE) , m_UserData(userData) , m_Timeout(nullptr) , m_Reroute(false) @@ -750,6 +789,7 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_FileID(0) , m_Reply(nullptr) , m_Type(type) + , m_UpdatePeriod(UpdatePeriod::NONE) , m_UserData(userData) , m_Timeout(nullptr) , m_Reroute(false) @@ -773,6 +813,30 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_FileID(fileID) , m_Reply(nullptr) , m_Type(type) + , m_UpdatePeriod(UpdatePeriod::NONE) + , m_UserData(userData) + , m_Timeout(nullptr) + , m_Reroute(false) + , m_ID(s_NextID.fetchAndAddAcquire(1)) + , m_URL(get_management_url(game)) + , m_SubModule(subModule) + , m_NexusGameID(game->nexusGameID()) + , m_GameName(game->gameNexusName()) + , m_Endorse(false) +{} + +NexusInterface::NXMRequestInfo::NXMRequestInfo(UpdatePeriod period + , NexusInterface::NXMRequestInfo::Type type + , QVariant userData + , const QString &subModule + , MOBase::IPluginGame const *game +) + : m_ModID(0) + , m_ModVersion("0") + , m_FileID(0) + , m_Reply(nullptr) + , m_Type(type) + , m_UpdatePeriod(period) , m_UserData(userData) , m_Timeout(nullptr) , m_Reroute(false) diff --git a/src/nexusinterface.h b/src/nexusinterface.h index 1c283335..06b19947 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -127,6 +127,15 @@ class NexusInterface : public QObject { Q_OBJECT +public: + + enum UpdatePeriod { + NONE, + DAY, + WEEK, + MONTH + }; + public: ~NexusInterface(); @@ -202,6 +211,15 @@ public: int requestModInfo(QString gameName, int modID, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + int requestUpdateInfo(QString gameName, NexusInterface::UpdatePeriod period, QObject *receiver, QVariant userData, + const QString &subModule) + { + return requestUpdateInfo(gameName, period, receiver, userData, subModule, getGame(gameName)); + } + + int requestUpdateInfo(QString gameName, NexusInterface::UpdatePeriod period, QObject *receiver, QVariant userData, + const QString &subModule, MOBase::IPluginGame const *game); + /** * @brief request nexus descriptions for multiple mods at once * @param modID id of the mod the caller is interested in @@ -378,6 +396,7 @@ signals: void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); + void nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int requestID); void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); @@ -415,8 +434,10 @@ private: TYPE_FILEINFO, TYPE_DOWNLOADURL, TYPE_TOGGLEENDORSEMENT, - TYPE_GETUPDATES + TYPE_GETUPDATES, + TYPE_CHECKUPDATES } m_Type; + UpdatePeriod m_UpdatePeriod; QVariant m_UserData; QTimer *m_Timeout; QString m_URL; @@ -430,6 +451,7 @@ private: NXMRequestInfo(int modID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); NXMRequestInfo(int modID, QString modVersion, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); NXMRequestInfo(int modID, int fileID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + NXMRequestInfo(UpdatePeriod period, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); private: static QAtomicInt s_NextID; -- cgit v1.3.1 From 2b9d2f32fbbc576c9367a24c0347e5870ff20117 Mon Sep 17 00:00:00 2001 From: Silarn Date: Mon, 11 Feb 2019 16:51:47 -0600 Subject: Endorsement and efficiency updates * Reduce update timeout to 5 minutes * Introduce bulk endorsement check replacing global mod info check * Fine tune performance of bulk update info query * Thread the update processing to prevent lockups --- src/mainwindow.cpp | 162 ++++++++++++++++++++++++++++++++++--------------- src/mainwindow.h | 13 ++-- src/modinfo.cpp | 37 ++++------- src/modinfo.h | 4 +- src/modinforegular.cpp | 3 + src/nexusinterface.cpp | 70 +++++++++++++++++---- src/nexusinterface.h | 9 ++- 7 files changed, 203 insertions(+), 95 deletions(-) (limited to 'src/modinfo.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 5e97a959..7592bd1e 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -261,7 +261,8 @@ MainWindow::MainWindow(QSettings &initSettings actionToToolButton(ui->actionEndorseMO); createEndorseWidget(); - ui->actionEndorseMO->setVisible(false); + + toggleMO2EndorseState(); for (QAction *action : ui->toolBar->actions()) { if (action->isSeparator()) { @@ -752,7 +753,7 @@ void MainWindow::createEndorseWidget() buttonMenu->addAction(endorseAction); QAction *wontEndorseAction = new QAction(tr("Won't Endorse"), buttonMenu); - connect(wontEndorseAction, SIGNAL(triggered()), this, SLOT(wontEndorse())); + connect(wontEndorseAction, SIGNAL(triggered()), this, SLOT(on_actionWontEndorseMO_triggered())); buttonMenu->addAction(wontEndorseAction); } @@ -3983,8 +3984,8 @@ void MainWindow::checkModsForUpdates() { statusBar()->show(); if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { - m_ModsToUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this); - m_RefreshProgress->setRange(0, m_ModsToUpdate); + ModInfo::checkAllForUpdate(&m_PluginContainer, this); + NexusInterface::instance(&m_PluginContainer)->requestEndorsementInfo(this, QVariant(), QString()); } else { QString apiKey; if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { @@ -5318,26 +5319,8 @@ void MainWindow::motdReceived(const QString &motd) m_OrganizerCore.settings().setMotDHash(hash); } } - - ui->actionEndorseMO->setVisible(false); -} - - -void MainWindow::notEndorsedYet() -{ - if (!Settings::instance().directInterface().value("wont_endorse_MO", false).toBool()) { - ui->actionEndorseMO->setVisible(true); - } -} - - -void MainWindow::wontEndorse() -{ - Settings::instance().directInterface().setValue("wont_endorse_MO", true); - ui->actionEndorseMO->setVisible(false); } - void MainWindow::on_dataTree_customContextMenuRequested(const QPoint &pos) { QTreeWidget *dataTree = findChild("dataTree"); @@ -5398,6 +5381,21 @@ void MainWindow::on_actionEndorseMO_triggered() } } +void MainWindow::on_actionWontEndorseMO_triggered() +{ + // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now + IPluginGame * game = m_OrganizerCore.getGame("skyrimse"); + if (!game) return; + + if (QMessageBox::question(this, tr("Abstain from Endorsing Mod Organizer"), + tr("Are you sure you want to abstain from endorsing Mod Organizer 2?\n" + "You will have to visit the mod page on the %1 Nexus site to change your mind.").arg( + NexusInterface::instance(&m_PluginContainer)->getGameURL(game->gameShortName())), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + NexusInterface::instance(&m_PluginContainer)->requestToggleEndorsement( + game->gameShortName(), game->nexusModOrganizerID(), m_OrganizerCore.getVersion().canonicalString(), false, this, QVariant(), QString()); + } +} void MainWindow::initDownloadView() { @@ -5448,21 +5446,10 @@ void MainWindow::updateDownloadView() m_OrganizerCore.downloadManager()->refreshList(); } -void MainWindow::modDetailsUpdated(bool) -{ - if (m_ModsToUpdate <= 0) { - statusBar()->hide(); - m_RefreshProgress->setVisible(false); - } else { - m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); - } -} - void MainWindow::modUpdateCheck(std::multimap IDs) { if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { - m_ModsToUpdate += ModInfo::manualUpdateCheck(&m_PluginContainer, this, IDs); - m_RefreshProgress->setRange(0, m_ModsToUpdate); + ModInfo::manualUpdateCheck(&m_PluginContainer, this, IDs); } else { QString apiKey; if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { @@ -5473,6 +5460,80 @@ void MainWindow::modUpdateCheck(std::multimap IDs) } } +void MainWindow::toggleMO2EndorseState() +{ + QToolButton *toolBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionEndorseMO)); + if (Settings::instance().endorsementIntegration()) { + ui->actionEndorseMO->setVisible(true); + if (Settings::instance().directInterface().contains("endorse_state")) { + ui->actionEndorseMO->setEnabled(false); + if (Settings::instance().directInterface().value("endorse_state").toString() == "Endorsed") { + ui->actionEndorseMO->setToolTip(tr("Thank you for endorsing MO2! :)")); + toolBtn->setToolTip(tr("Thank you for endorsing MO2! :)")); + } else if (Settings::instance().directInterface().value("endorse_state").toString() == "Abstained") { + ui->actionEndorseMO->setToolTip(tr("Please reconsider endorsing MO2 on Nexus!")); + toolBtn->setToolTip(tr("Please reconsider endorsing MO2 on Nexus!")); + } + } else { + ui->actionEndorseMO->setEnabled(true); + } + } else + ui->actionEndorseMO->setVisible(false); +} + +void MainWindow::nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int) +{ + QVariantList data = resultData.toList(); + std::multimap> sorted; + QStringList games = m_OrganizerCore.managedGame()->validShortNames(); + games += m_OrganizerCore.managedGame()->gameShortName(); + bool searchedMO2NexusGame = false; + for (auto endorsementData : data) { + QVariantMap endorsement = endorsementData.toMap(); + std::pair data = std::make_pair(endorsement["mod_id"].toInt(), endorsement["status"].toString()); + sorted.insert(std::pair>(endorsement["domain_name"].toString(), data)); + } + for (auto game : games) { + IPluginGame *gamePlugin = m_OrganizerCore.getGame(game); + if (gamePlugin->gameShortName().compare("SkyrimSE", Qt::CaseInsensitive) == 0) + searchedMO2NexusGame = true; + auto iter = sorted.equal_range(gamePlugin->gameNexusName()); + for (auto result = iter.first; result != iter.second; ++result) { + std::vector modsList = ModInfo::getByModID(result->first, result->second.first); + + for (auto mod : modsList) { + if (result->second.second == "Endorsed") + mod->setIsEndorsed(true); + else if (result->second.second == "Abstained") + mod->setNeverEndorse(); + else + mod->setIsEndorsed(false); + } + + if (Settings::instance().endorsementIntegration()) { + if (result->first == "skyrimspecialedition" && result->second.first == gamePlugin->nexusModOrganizerID()) { + Settings::instance().directInterface().setValue("endorse_state", result->second.second); + toggleMO2EndorseState(); + } + } + } + } + + if (!searchedMO2NexusGame && Settings::instance().endorsementIntegration()) { + auto gamePlugin = m_OrganizerCore.getGame("SkyrimSE"); + if (gamePlugin) { + auto iter = sorted.equal_range(gamePlugin->gameNexusName()); + for (auto result = iter.first; result != iter.second; ++result) { + if (result->second.first == gamePlugin->nexusModOrganizerID()) { + Settings::instance().directInterface().setValue("endorse_state", result->second.second); + toggleMO2EndorseState(); + break; + } + } + } + } +} + void MainWindow::nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int) { QString gameNameReal; @@ -5483,7 +5544,20 @@ void MainWindow::nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVa } } QVariantList resultList = resultData.toList(); - std::set> finalMods = ModInfo::filteredMods(gameNameReal, resultList, userData.toBool(), true); + + QFutureWatcher>> *watcher = new QFutureWatcher>>(); + QObject::connect(watcher, &QFutureWatcher>>::finished, this, &MainWindow::finishUpdateInfo); + QFuture>> future = QtConcurrent::run([=]() -> std::set> { + return ModInfo::filteredMods(gameNameReal, resultList, userData.toBool(), true); + }); + watcher->setFuture(future); +} + +void MainWindow::finishUpdateInfo() +{ + QFutureWatcher>> *watcher = static_cast>> *>(sender()); + + auto finalMods = watcher->result(); if (finalMods.empty()) { qInfo("None of your mods appear to have had recent file updates."); @@ -5501,6 +5575,9 @@ void MainWindow::nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVa for (auto game : organizedGames) NexusInterface::instance(&m_PluginContainer)->requestUpdates(game.second, this, QVariant(), game.first, QString()); + + disconnect(sender()); + delete sender(); } void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) @@ -5508,7 +5585,6 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD QVariantMap resultInfo = resultData.toMap(); QList files = resultInfo["files"].toList(); QList fileUpdates = resultInfo["file_updates"].toList(); - m_ModsToUpdate--; QString gameNameReal; for (IPluginGame *game : m_PluginContainer.plugins()) { if (game->gameNexusName() == gameName) { @@ -5578,19 +5654,10 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD // Scrape mod data here so we can use the mod version if no file update was located requiresInfo = true; } - - if (mod->getLastNexusQuery().addDays(1) <= QDateTime::currentDateTimeUtc()) - requiresInfo = true; } if (requiresInfo) NexusInterface::instance(&m_PluginContainer)->requestModInfo(gameName, modID, this, QVariant(), QString()); - - if (--m_ModsToUpdate <= 0) { - statusBar()->hide(); - } else { - m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); - } } void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) @@ -5663,11 +5730,6 @@ void MainWindow::nxmDownloadURLs(QString, int, int, QVariant, QVariant resultDat void MainWindow::nxmRequestFailed(QString gameName, int modID, int, QVariant, int, QNetworkReply::NetworkError error, const QString &errorString) { - if (modID == -1) { - // must be the update-check that failed - m_ModsToUpdate = 0; - statusBar()->hide(); - } if (error == QNetworkReply::ContentAccessDenied || error == QNetworkReply::ContentNotFoundError) { QString gameNameReal; for (IPluginGame *game : m_PluginContainer.plugins()) { diff --git a/src/mainwindow.h b/src/mainwindow.h index d2704b72..a0c043a1 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -64,6 +64,7 @@ namespace MOShared { class DirectoryEntry; } #include #include #include +#include class QAction; class QAbstractItemModel; @@ -303,6 +304,8 @@ private: void sendSelectedModsToPriority(int newPriority); void sendSelectedPluginsToPriority(int newPriority); + void toggleMO2EndorseState(); + private: static const char *PATTERN_BACKUP_GLOB; @@ -341,8 +344,6 @@ private: CategoryFactory &m_CategoryFactory; - int m_ModsToUpdate; - bool m_LoginAttempted; QTimer m_CheckBSATimer; @@ -494,8 +495,6 @@ private slots: void updateAvailable(); void motdReceived(const QString &motd); - void notEndorsedYet(); - void wontEndorse(); void originModified(int originID); @@ -504,12 +503,13 @@ private slots: void addPrimaryCategoryCandidates(); - void modDetailsUpdated(bool success); - void modInstalled(const QString &modName); void modUpdateCheck(std::multimap IDs); + void finishUpdateInfo(); + + void nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int); void nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int requestID); void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); @@ -622,6 +622,7 @@ private slots: // ui slots void on_actionSettings_triggered(); void on_actionUpdate_triggered(); void on_actionEndorseMO_triggered(); + void on_actionWontEndorseMO_triggered(); void on_bsaList_customContextMenuRequested(const QPoint &pos); void on_clearFiltersButton_clicked(); diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 3e694baf..938a6418 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -284,16 +284,8 @@ ModInfo::ModInfo(PluginContainer *pluginContainer) } -int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver) +void ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver) { - int result = 0; - - // MO2 endorsement status is no longer available via this method - an alternative must be found - //IPluginGame const *game = pluginContainer->managedGame("Skyrim Special Edition"); - //if (game && game->nexusModOrganizerID()) { - // NexusInterface::instance(pluginContainer)->requestUpdates(game->nexusModOrganizerID(), receiver, QVariant(), game->gameShortName(), QString()); - //} - QDateTime earliest = QDateTime::currentDateTimeUtc(); QDateTime latest; std::set games; @@ -315,8 +307,6 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv } } - result = organizedGames.size(); - if (organizedGames.empty()) qWarning("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests."); @@ -336,8 +326,6 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv for (auto gameName : games) NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::DAY, receiver, QVariant(false), QString()); } - - return result; } std::set> ModInfo::filteredMods(QString gameName, QVariantList updateData, bool addOldMods, bool markUpdated) @@ -345,10 +333,12 @@ std::set> ModInfo::filteredMods(QString gameName, QVaria std::set> finalMods; for (QVariant result : updateData) { QVariantMap update = result.toMap(); - for (auto mod : s_Collection) - if (mod->getNexusID() == update["mod_id"].toInt() && mod->getGameName().compare(gameName, Qt::CaseInsensitive) == 0) - if (mod->getLastNexusUpdate() > QDateTime::fromSecsSinceEpoch(update["latest_file_update"].toInt(), Qt::UTC)) - finalMods.insert(mod); + std::copy_if(s_Collection.begin(), s_Collection.end(), std::inserter(finalMods, finalMods.end()), [=](QSharedPointer info) -> bool { + if (info->getNexusID() == update["mod_id"].toInt() && info->getGameName().compare(gameName, Qt::CaseInsensitive) == 0) + if (info->getLastNexusUpdate() > QDateTime::fromSecsSinceEpoch(update["latest_file_update"].toInt(), Qt::UTC)) + return true; + return false; + }); } if (addOldMods) @@ -358,20 +348,21 @@ std::set> ModInfo::filteredMods(QString gameName, QVaria if (markUpdated) { std::set> updates; - for (auto mod : s_Collection) - if (mod->getGameName().compare(gameName, Qt::CaseInsensitive) == 0 && mod->canBeUpdated()) - updates.insert(mod); + std::copy_if(s_Collection.begin(), s_Collection.end(), std::inserter(updates, updates.end()), [=](QSharedPointer info) -> bool { + if (info->getGameName().compare(gameName, Qt::CaseInsensitive) == 0 && info->canBeUpdated()) + return true; + return false; + }); std::set> diff; std::set_difference(updates.begin(), updates.end(), finalMods.begin(), finalMods.end(), std::inserter(diff, diff.end())); for (auto skipped : diff) { skipped->setLastNexusUpdate(QDateTime::currentDateTimeUtc()); - skipped->updateNXMInfo(); } } return finalMods; } -int ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap IDs) +void ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap IDs) { std::vector> mods; std::set> organizedGames; @@ -402,8 +393,6 @@ int ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiv } else { qInfo("None of the selected mods can be updated."); } - - return organizedGames.size(); } diff --git a/src/modinfo.h b/src/modinfo.h index 8c234225..c3115685 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -188,12 +188,12 @@ public: /** * @brief run a limited batch of mod update checks for "newest version" information */ - static int manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap IDs); + static void manualUpdateCheck(PluginContainer *pluginContainer, QObject *receiver, std::multimap IDs); /** * @brief query nexus information for every mod and update the "newest version" information **/ - static int checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver); + static void checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver); static std::set> filteredMods(QString gameName, QVariantList updateData, bool addOldMods = false, bool markUpdated = false); diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 9532bf6d..ac1c46d6 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -507,6 +507,8 @@ bool ModInfoRegular::canBeUpdated() const QDateTime ModInfoRegular::getExpires() const { + return m_LastNexusUpdate.addSecs(300); + /* switch (Settings::instance().nexusUpdateStrategy()) { case Settings::NexusUpdateStrategy::Flexible: { qint64 diff = m_NexusLastModified.msecsTo(QDateTime::currentDateTimeUtc()); @@ -530,6 +532,7 @@ QDateTime ModInfoRegular::getExpires() const return m_LastNexusUpdate.addSecs(86400); } break; } + */ } std::vector ModInfoRegular::getFlags() const diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index abddbd3e..e657dfc1 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -125,6 +125,15 @@ void NexusBridge::nxmDownloadURLsAvailable(QString gameName, int modID, int file } } +void NexusBridge::nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit endorsementsAvailable(userData, resultData); + } +} + void NexusBridge::nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) { std::set::iterator iter = m_RequestIDs.find(requestID); @@ -455,6 +464,21 @@ int NexusInterface::requestDownloadURL(QString gameName, int modID, int fileID, return requestInfo.m_ID; } +int NexusInterface::requestEndorsementInfo(QObject *receiver, QVariant userData, const QString &subModule) +{ + NXMRequestInfo requestInfo(NXMRequestInfo::TYPE_ENDORSEMENTS, userData, subModule); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmEndorsementsAvailable(QVariant, QVariant, int)), + receiver, SLOT(nxmEndorsementsAvailable(QVariant, QVariant, int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), + receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game) @@ -478,12 +502,6 @@ int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QStrin return -1; } -bool NexusInterface::requiresLogin(const NXMRequestInfo &info) -{ - return (info.m_Type == NXMRequestInfo::TYPE_TOGGLEENDORSEMENT) - || (info.m_Type == NXMRequestInfo::TYPE_DOWNLOADURL); -} - IPluginGame* NexusInterface::getGame(QString gameName) const { auto gamePlugins = m_PluginContainer->plugins(); @@ -516,7 +534,7 @@ void NexusInterface::nextRequest() return; } - if (requiresLogin(m_RequestQueue.head()) && !getAccessManager()->validated()) { + if (!getAccessManager()->validated()) { if (!getAccessManager()->validateAttempted()) { emit needLogin(); return; @@ -581,6 +599,9 @@ void NexusInterface::nextRequest() else url = QString("%1/games/%2/mods/%3/files/%4/download_link").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID); } break; + case NXMRequestInfo::TYPE_ENDORSEMENTS: { + url = QString("%1/user/endorsements").arg(info.m_URL); + } break; case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { QString endorse = info.m_Endorse ? "endorse" : "abstain"; url = QString("%1/games/%2/mods/%3/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(endorse); @@ -674,6 +695,9 @@ void NexusInterface::requestFinished(std::list::iterator iter) case NXMRequestInfo::TYPE_DOWNLOADURL: { emit nxmDownloadURLsAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID); } break; + case NXMRequestInfo::TYPE_ENDORSEMENTS: { + emit nxmEndorsementsAvailable(iter->m_UserData, result, iter->m_ID); + } break; case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { emit nxmEndorsementToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID); } break; @@ -747,7 +771,7 @@ void NexusInterface::requestTimeout() } namespace { - QString get_management_url(MOBase::IPluginGame const *game) + QString get_management_url() { return "https://api.nexusmods.com/v1"; } @@ -769,7 +793,7 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_Timeout(nullptr) , m_Reroute(false) , m_ID(s_NextID.fetchAndAddAcquire(1)) - , m_URL(get_management_url(game)) + , m_URL(get_management_url()) , m_SubModule(subModule) , m_NexusGameID(game->nexusGameID()) , m_GameName(game->gameNexusName()) @@ -794,7 +818,7 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_Timeout(nullptr) , m_Reroute(false) , m_ID(s_NextID.fetchAndAddAcquire(1)) - , m_URL(get_management_url(game)) + , m_URL(get_management_url()) , m_SubModule(subModule) , m_NexusGameID(game->nexusGameID()) , m_GameName(game->gameNexusName()) @@ -818,13 +842,35 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID , m_Timeout(nullptr) , m_Reroute(false) , m_ID(s_NextID.fetchAndAddAcquire(1)) - , m_URL(get_management_url(game)) + , m_URL(get_management_url()) , m_SubModule(subModule) , m_NexusGameID(game->nexusGameID()) , m_GameName(game->gameNexusName()) , m_Endorse(false) {} +NexusInterface::NXMRequestInfo::NXMRequestInfo(Type type + , QVariant userData + , const QString &subModule +) + : m_ModID(0) + , m_ModVersion("0") + , m_FileID(0) + , m_Reply(nullptr) + , m_Type(type) + , m_UpdatePeriod(UpdatePeriod::NONE) + , m_UserData(userData) + , m_Timeout(nullptr) + , m_Reroute(false) + , m_ID(s_NextID.fetchAndAddAcquire(1)) + , m_URL(get_management_url()) + , m_SubModule(subModule) + , m_NexusGameID(0) + , m_GameName("") + , m_Endorse(false) +{} + + NexusInterface::NXMRequestInfo::NXMRequestInfo(UpdatePeriod period , NexusInterface::NXMRequestInfo::Type type , QVariant userData @@ -841,7 +887,7 @@ NexusInterface::NXMRequestInfo::NXMRequestInfo(UpdatePeriod period , m_Timeout(nullptr) , m_Reroute(false) , m_ID(s_NextID.fetchAndAddAcquire(1)) - , m_URL(get_management_url(game)) + , m_URL(get_management_url()) , m_SubModule(subModule) , m_NexusGameID(game->nexusGameID()) , m_GameName(game->gameNexusName()) diff --git a/src/nexusinterface.h b/src/nexusinterface.h index 06b19947..ac9e466b 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -104,6 +104,7 @@ public slots: void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + void nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorMessage); @@ -298,6 +299,10 @@ public: **/ int requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + + int requestEndorsementInfo(QObject *receiver, QVariant userData, const QString &subModule); + + /** * @param gameName the game short name to support multiple game sources * @brief toggle endorsement state of the mod @@ -401,6 +406,7 @@ signals: void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID); + void nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int requestID); void nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString); void requestsChanged(int queueCount, std::tuple requestsRemaining); @@ -433,6 +439,7 @@ private: TYPE_FILES, TYPE_FILEINFO, TYPE_DOWNLOADURL, + TYPE_ENDORSEMENTS, TYPE_TOGGLEENDORSEMENT, TYPE_GETUPDATES, TYPE_CHECKUPDATES @@ -451,6 +458,7 @@ private: NXMRequestInfo(int modID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); NXMRequestInfo(int modID, QString modVersion, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); NXMRequestInfo(int modID, int fileID, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); + NXMRequestInfo(Type type, QVariant userData, const QString &subModule); NXMRequestInfo(UpdatePeriod period, Type type, QVariant userData, const QString &subModule, MOBase::IPluginGame const *game); private: @@ -464,7 +472,6 @@ private: NexusInterface(PluginContainer *pluginContainer); void nextRequest(); void requestFinished(std::list::iterator iter); - bool requiresLogin(const NXMRequestInfo &info); MOBase::IPluginGame *getGame(QString gameName) const; QString getOldModsURL(QString gameName) const; -- cgit v1.3.1 From d2120a9c555b3f446dea44e91ac29e128a242c1b Mon Sep 17 00:00:00 2001 From: Silarn Date: Sat, 16 Feb 2019 18:38:55 -0600 Subject: Extend the update date back one hour to help prevent time mismatches - Server time and local time may not align perfectly for bulk API --- src/modinfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/modinfo.cpp') diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 938a6418..4d30d770 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -335,7 +335,7 @@ std::set> ModInfo::filteredMods(QString gameName, QVaria QVariantMap update = result.toMap(); std::copy_if(s_Collection.begin(), s_Collection.end(), std::inserter(finalMods, finalMods.end()), [=](QSharedPointer info) -> bool { if (info->getNexusID() == update["mod_id"].toInt() && info->getGameName().compare(gameName, Qt::CaseInsensitive) == 0) - if (info->getLastNexusUpdate() > QDateTime::fromSecsSinceEpoch(update["latest_file_update"].toInt(), Qt::UTC)) + if (info->getLastNexusUpdate().addSecs(-3600) > QDateTime::fromSecsSinceEpoch(update["latest_file_update"].toInt(), Qt::UTC)) return true; return false; }); -- cgit v1.3.1 From 01203bb467c94888725afaf6404e46ea0540d3fa Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Thu, 21 Feb 2019 21:56:09 -0600 Subject: Allow force-check updates to bypass update timeouts --- src/modinfo.cpp | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/modinfo.cpp') diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 4d30d770..661681d8 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -375,6 +375,9 @@ void ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *recei std::remove_if(mods.begin(), mods.end(), [](ModInfo::Ptr mod) -> bool { return mod->getNexusID() <= 0; }), mods.end() ); + for (auto mod : mods) { + mod->setLastNexusUpdate(QDateTime()); + } std::sort(mods.begin(), mods.end(), [](QSharedPointer a, QSharedPointer b) -> bool { return a->getLastNexusUpdate() < b->getLastNexusUpdate(); -- cgit v1.3.1 From e8e7f70dedd8808d44cc3156ec2687c83153f5e9 Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Sat, 9 Mar 2019 21:41:20 -0600 Subject: Allow selecting overwrite to trigger mod conflict highlights --- src/modflagicondelegate.cpp | 4 ++++ src/modinfo.cpp | 7 ++++--- src/modinfo.h | 3 ++- src/modinfooverwrite.cpp | 5 ++++- src/modinfooverwrite.h | 6 +++--- src/modinfowithconflictinfo.cpp | 20 ++++++++++---------- 6 files changed, 27 insertions(+), 18 deletions(-) (limited to 'src/modinfo.cpp') diff --git a/src/modflagicondelegate.cpp b/src/modflagicondelegate.cpp index fbd951eb..c3142962 100644 --- a/src/modflagicondelegate.cpp +++ b/src/modflagicondelegate.cpp @@ -36,6 +36,10 @@ QList ModFlagIconDelegate::getIcons(const QModelIndex &index) const { ModInfo::Ptr info = ModInfo::getByIndex(modid.toInt()); std::vector flags = info->getFlags(); + // Don't do flags for overwrite + if (std::find(flags.begin(), flags.end(),ModInfo::FLAG_OVERWRITE) != flags.end()) + return result; + // insert conflict icons to provide nicer alignment { // insert loose file conflicts first auto iter = std::find_first_of(flags.begin(), flags.end(), diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 661681d8..13b40aba 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -114,11 +114,12 @@ QString ModInfo::getContentTypeName(int contentType) } } -void ModInfo::createFromOverwrite(PluginContainer *pluginContainer) +void ModInfo::createFromOverwrite(PluginContainer *pluginContainer, + MOShared::DirectoryEntry **directoryStructure) { QMutexLocker locker(&s_Mutex); - s_Collection.push_back(ModInfo::Ptr(new ModInfoOverwrite(pluginContainer))); + s_Collection.push_back(ModInfo::Ptr(new ModInfoOverwrite(pluginContainer, directoryStructure))); } unsigned int ModInfo::getNumMods() @@ -255,7 +256,7 @@ void ModInfo::updateFromDisc(const QString &modDirectory, } } - createFromOverwrite(pluginContainer); + createFromOverwrite(pluginContainer, directoryStructure); std::sort(s_Collection.begin(), s_Collection.end(), ModInfo::ByName); diff --git a/src/modinfo.h b/src/modinfo.h index 365dfe50..81f59b61 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -750,7 +750,8 @@ protected: private: - static void createFromOverwrite(PluginContainer *pluginContainer); + static void createFromOverwrite(PluginContainer *pluginContainer, + MOShared::DirectoryEntry **directoryStructure); protected: diff --git a/src/modinfooverwrite.cpp b/src/modinfooverwrite.cpp index 0e428483..37c8c650 100644 --- a/src/modinfooverwrite.cpp +++ b/src/modinfooverwrite.cpp @@ -6,7 +6,8 @@ #include #include -ModInfoOverwrite::ModInfoOverwrite(PluginContainer *pluginContainer) : ModInfo(pluginContainer) +ModInfoOverwrite::ModInfoOverwrite(PluginContainer *pluginContainer, MOShared::DirectoryEntry **directoryStructure) + : ModInfoWithConflictInfo(pluginContainer, directoryStructure) { testValid(); } @@ -31,6 +32,8 @@ std::vector ModInfoOverwrite::getFlags() const result.push_back(FLAG_OVERWRITE); if (m_PluginSelected) result.push_back(FLAG_PLUGIN_SELECTED); + for (auto flag : ModInfoWithConflictInfo::getFlags()) + result.push_back(flag); return result; } diff --git a/src/modinfooverwrite.h b/src/modinfooverwrite.h index ff0d8cd4..c5f58c2e 100644 --- a/src/modinfooverwrite.h +++ b/src/modinfooverwrite.h @@ -1,11 +1,11 @@ #ifndef MODINFOOVERWRITE_H #define MODINFOOVERWRITE_H -#include "modinfo.h" +#include "modinfowithconflictinfo.h" #include -class ModInfoOverwrite : public ModInfo +class ModInfoOverwrite : public ModInfoWithConflictInfo { Q_OBJECT @@ -67,7 +67,7 @@ public: private: - ModInfoOverwrite(PluginContainer *pluginContainer); + ModInfoOverwrite(PluginContainer *pluginContainer, MOShared::DirectoryEntry **directoryStructure ); }; diff --git a/src/modinfowithconflictinfo.cpp b/src/modinfowithconflictinfo.cpp index 744f323c..7a2b0071 100644 --- a/src/modinfowithconflictinfo.cpp +++ b/src/modinfowithconflictinfo.cpp @@ -46,16 +46,16 @@ std::vector ModInfoWithConflictInfo::getFlags() const default: { /* NOP */ } } switch (isArchiveConflicted()) { - case CONFLICT_MIXED: { - result.push_back(ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED); - } break; - case CONFLICT_OVERWRITE: { - result.push_back(ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE); - } break; - case CONFLICT_OVERWRITTEN: { - result.push_back(ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN); - } break; - default: { /* NOP */ } + case CONFLICT_MIXED: { + result.push_back(ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED); + } break; + case CONFLICT_OVERWRITE: { + result.push_back(ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE); + } break; + case CONFLICT_OVERWRITTEN: { + result.push_back(ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN); + } break; + default: { /* NOP */ } } return result; } -- cgit v1.3.1 From 5b6f16f06ae06393f0306f39e25265d3f969760b Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Fri, 22 Mar 2019 00:52:00 -0500 Subject: Fix reported number of mods checking for update when multiple mods are selected --- src/modinfo.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'src/modinfo.cpp') diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 13b40aba..0afd92e6 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -114,7 +114,7 @@ QString ModInfo::getContentTypeName(int contentType) } } -void ModInfo::createFromOverwrite(PluginContainer *pluginContainer, +void ModInfo::createFromOverwrite(PluginContainer *pluginContainer, MOShared::DirectoryEntry **directoryStructure) { QMutexLocker locker(&s_Mutex); @@ -369,8 +369,17 @@ void ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *recei std::set> organizedGames; for (auto ID : IDs) { - auto matchedMods = getByModID(ID.first, ID.second); - mods.insert(mods.end(), matchedMods.begin(), matchedMods.end()); + for (auto matchedMod : getByModID(ID.first, ID.second)) { + bool alreadyMatched = false; + for (auto mod : mods) { + if (mod == matchedMod) { + alreadyMatched = true; + break; + } + } + if (!alreadyMatched) + mods.push_back(matchedMod); + } } mods.erase( std::remove_if(mods.begin(), mods.end(), [](ModInfo::Ptr mod) -> bool { return mod->getNexusID() <= 0; }), -- cgit v1.3.1 From c3ff652ff8c612054fa102234677c74fcd19a51c Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Fri, 22 Mar 2019 17:33:10 -0500 Subject: Only change to the update filter if mods are being checked for update or mods have updates available --- src/mainwindow.cpp | 24 ++++++++++++++++++------ src/modinfo.cpp | 18 ++++++++++++++++-- src/modinfo.h | 12 +++++++++++- 3 files changed, 45 insertions(+), 9 deletions(-) (limited to 'src/modinfo.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 669820be..8fe786ba 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -4075,28 +4075,40 @@ void MainWindow::saveArchiveList() void MainWindow::checkModsForUpdates() { - statusBar()->show(); + bool checkingModsForUpdate = false; if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->validated()) { - ModInfo::checkAllForUpdate(&m_PluginContainer, this); + checkingModsForUpdate = ModInfo::checkAllForUpdate(&m_PluginContainer, this); NexusInterface::instance(&m_PluginContainer)->requestEndorsementInfo(this, QVariant(), QString()); NexusInterface::instance(&m_PluginContainer)->requestTrackingInfo(this, QVariant(), QString()); } else { QString apiKey; if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) { m_OrganizerCore.doAfterLogin([this] () { this->checkModsForUpdates(); }); + statusBar()->show(); NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey); } else { qWarning("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus."); } } - m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)); - for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) { - if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { - ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i)); + bool updatesAvailable = false; + for (auto mod : m_OrganizerCore.modList()->allMods()) { + ModInfo::Ptr modInfo = ModInfo::getByName(mod); + if (modInfo->updateAvailable()) { + updatesAvailable = true; break; } } + + if (updatesAvailable || checkingModsForUpdate) { + m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)); + for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) { + if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { + ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i)); + break; + } + } + } } void MainWindow::changeVersioningScheme() { diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 0afd92e6..28e9b8f2 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -166,6 +166,14 @@ std::vector ModInfo::getByModID(QString game, int modID) } +ModInfo::Ptr ModInfo::getByName(const QString &name) +{ + QMutexLocker locker(&s_Mutex); + + return s_Collection[ModInfo::getIndex(name)]; +} + + bool ModInfo::removeMod(unsigned int index) { QMutexLocker locker(&s_Mutex); @@ -285,8 +293,10 @@ ModInfo::ModInfo(PluginContainer *pluginContainer) } -void ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver) +bool ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver) { + bool updatesAvailable = true; + QDateTime earliest = QDateTime::currentDateTimeUtc(); QDateTime latest; std::set games; @@ -308,8 +318,10 @@ void ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *recei } } - if (organizedGames.empty()) + if (organizedGames.empty()) { qWarning("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests."); + updatesAvailable = false; + } for (auto game : organizedGames) { NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString()); @@ -327,6 +339,8 @@ void ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *recei for (auto gameName : games) NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::DAY, receiver, QVariant(false), QString()); } + + return updatesAvailable; } std::set> ModInfo::filteredMods(QString gameName, QVariantList updateData, bool addOldMods, bool markUpdated) diff --git a/src/modinfo.h b/src/modinfo.h index 81f59b61..f1d816fe 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -167,6 +167,15 @@ public: **/ static std::vector getByModID(QString game, int modID); + /** + * @brief retrieve a ModInfo object based on its name + * + * @param name the name to look up + * @return a reference counting pointer to the mod info + * @note since the pointer is reference counter, the pointer remains valid even if the collection is refreshed in a different thread + **/ + static ModInfo::Ptr getByName(const QString &name); + /** * @brief remove a mod by index * @@ -199,8 +208,9 @@ public: /** * @brief query nexus information for every mod and update the "newest version" information + * @return true if any mods are checked for update **/ - static void checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver); + static bool checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver); static std::set> filteredMods(QString gameName, QVariantList updateData, bool addOldMods = false, bool markUpdated = false); -- cgit v1.3.1 From 1f50d61ecb236b3ff72e650c1fb45aba448a5bc5 Mon Sep 17 00:00:00 2001 From: LostDragonist Date: Fri, 22 Mar 2019 18:00:13 -0500 Subject: Fix update filter check --- src/modinfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/modinfo.cpp') diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 28e9b8f2..ad5c5fb9 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -350,7 +350,7 @@ std::set> ModInfo::filteredMods(QString gameName, QVaria QVariantMap update = result.toMap(); std::copy_if(s_Collection.begin(), s_Collection.end(), std::inserter(finalMods, finalMods.end()), [=](QSharedPointer info) -> bool { if (info->getNexusID() == update["mod_id"].toInt() && info->getGameName().compare(gameName, Qt::CaseInsensitive) == 0) - if (info->getLastNexusUpdate().addSecs(-3600) > QDateTime::fromSecsSinceEpoch(update["latest_file_update"].toInt(), Qt::UTC)) + if (info->getLastNexusUpdate() > QDateTime::fromSecsSinceEpoch(update["latest_file_update"].toInt(), Qt::UTC).addSecs(-3600)) return true; return false; }); -- cgit v1.3.1 From 5ea25f03ce9f674cd539c08ed5895eadde52e8ce Mon Sep 17 00:00:00 2001 From: Silarn Date: Fri, 12 Apr 2019 21:28:47 -0500 Subject: Rework logic - we want updates that are newer than the last update - 1hr --- src/modinfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/modinfo.cpp') diff --git a/src/modinfo.cpp b/src/modinfo.cpp index ad5c5fb9..c1974152 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -350,7 +350,7 @@ std::set> ModInfo::filteredMods(QString gameName, QVaria QVariantMap update = result.toMap(); std::copy_if(s_Collection.begin(), s_Collection.end(), std::inserter(finalMods, finalMods.end()), [=](QSharedPointer info) -> bool { if (info->getNexusID() == update["mod_id"].toInt() && info->getGameName().compare(gameName, Qt::CaseInsensitive) == 0) - if (info->getLastNexusUpdate() > QDateTime::fromSecsSinceEpoch(update["latest_file_update"].toInt(), Qt::UTC).addSecs(-3600)) + if (info->getLastNexusUpdate().addSecs(-3600) < QDateTime::fromSecsSinceEpoch(update["latest_file_update"].toInt(), Qt::UTC)) return true; return false; }); -- cgit v1.3.1