diff --git a/ChangeLog b/ChangeLog index ddc418551..27ce67539 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,6 +12,8 @@ SPELLS: * New configuration format: http://wiki.vcmi.eu/index.php?title=Spell_Format MODS: +* Support for submods - mod may have their own "submods" located in /Mods directory +* Mods may provide their own changelogs and screenshots that will be visible in Launcher * Mods cas now add new (offensive, buffs, debuffs) spells and change existing 0.94 -> 0.95 (Mar 01 2014) diff --git a/config/schemas/mod.json b/config/schemas/mod.json index e93b5d215..be0e5b9fd 100644 --- a/config/schemas/mod.json +++ b/config/schemas/mod.json @@ -31,6 +31,16 @@ "description": "Author of the mod. Can be nickname, real name or name of team" }, + "licenseName" : { + "type":"string", + "description": "Name of the license, recommended is Creative Commons Attribution-ShareAlike" + }, + + "licenseURL" : { + "type":"string", + "description": "Url to license text, e.g. http://creativecommons.org/licenses/by-sa/4.0/deed" + }, + "contact" : { "type":"string", "description": "Home page of mod or link to forum thread" @@ -47,6 +57,11 @@ "items": { "type":"string" } }, + "keepDisabled" : { + "type":"boolean", + "description": "If set to true, mod will not be enabled automatically on install" + }, + "artifacts": { "type":"array", "description": "List of configuration files for artifacts", @@ -72,12 +87,21 @@ "description": "List of configuration files for heroes", "items": { "type":"string", "format" : "textFile" } }, - "spells": { + "spells": { "type":"array", "description": "List of configuration files for spells", "items": { "type":"string", "format" : "textFile" } }, + "changelog" : { + "type":"object", + "description": "List of changes/new features in each version", + "additionalProperties" : { + "type" : "array", + "items" : { "type":"string" } + } + }, + "filesystem": { "type":"object", "description": "Optional, description on how files are organized in your mod. In most cases you do not need to use this field", diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index ecad516ce..0fe324636 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -10,6 +10,7 @@ set(launcher_modmanager_SRCS modManager/cmodlistmodel_moc.cpp modManager/cmodlistview_moc.cpp modManager/cmodmanager.cpp + modManager/imageviewer.cpp ) set(launcher_settingsview_SRCS @@ -28,6 +29,7 @@ set(launcher_SRCS set(launcher_FORMS modManager/cmodlistview_moc.ui + modManager/imageviewer.ui settingsView/csettingsview_moc.ui mainwindow_moc.ui ) diff --git a/launcher/mainwindow_moc.ui b/launcher/mainwindow_moc.ui index 7f746adcc..2aa753c8b 100644 --- a/launcher/mainwindow_moc.ui +++ b/launcher/mainwindow_moc.ui @@ -97,7 +97,7 @@ false - -1 + 0 diff --git a/launcher/modManager/cdownloadmanager_moc.cpp b/launcher/modManager/cdownloadmanager_moc.cpp index ce71f1248..b25c42d7d 100644 --- a/launcher/modManager/cdownloadmanager_moc.cpp +++ b/launcher/modManager/cdownloadmanager_moc.cpp @@ -113,3 +113,13 @@ void CDownloadManager::downloadProgressChanged(qint64 bytesReceived, qint64 byte emit downloadProgress(received, total); } + +bool CDownloadManager::downloadInProgress(const QUrl &url) +{ + for (auto & entry : currentDownloads) + { + if (entry.reply->url() == url) + return true; + } + return false; +} diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index 628e58c05..e5f71e131 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -271,3 +271,16 @@ QVector CModList::getModList() const } return modList; } + +QVector CModList::getChildren(QString parent) const +{ + QVector children; + + int depth = parent.count('.') + 1; + for (const QString & mod : getModList()) + { + if (mod.count('.') == depth && mod.startsWith(parent)) + children.push_back(mod); + } + return children; +} diff --git a/launcher/modManager/cmodlist.h b/launcher/modManager/cmodlist.h index f28a6e516..38b1eeeda 100644 --- a/launcher/modManager/cmodlist.h +++ b/launcher/modManager/cmodlist.h @@ -79,4 +79,6 @@ public: // returns list of all available mods QVector getModList() const; + + QVector getChildren(QString parent) const; }; diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index 9ff609beb..8ed2001c8 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -1,6 +1,7 @@ #include "StdInc.h" #include "cmodlistview_moc.h" #include "ui_cmodlistview_moc.h" +#include "imageviewer.h" #include #include @@ -68,7 +69,7 @@ CModListView::CModListView(QWidget *parent) : ui->progressWidget->setVisible(false); dlManager = nullptr; - //loadRepositories(); + loadRepositories(); hideModInfo(); } @@ -106,6 +107,7 @@ void CModListView::showModInfo() { ui->modInfoWidget->show(); ui->hideModInfoButton->setArrowType(Qt::RightArrow); + loadScreenshots(); } void CModListView::hideModInfo() @@ -135,25 +137,60 @@ static QString replaceIfNotEmpty(QStringList value, QString pattern) return ""; } +QString CModListView::genChangelogText(CModEntry &mod) +{ + QString headerTemplate = "

%1:

"; + QString entryBegin = "

    "; + QString entryEnd = "

"; + QString entryLine = "
  • %1
  • "; + //QString versionSeparator = "
    "; + + QString result; + + QVariantMap changelog = mod.getValue("changelog").toMap(); + QList versions = changelog.keys(); + + std::sort(versions.begin(), versions.end(), [](QString lesser, QString greater) + { + return !CModEntry::compareVersions(lesser, greater); + }); + + for (auto & version : versions) + { + result += headerTemplate.arg(version); + result += entryBegin; + for (auto & line : changelog.value(version).toStringList()) + result += entryLine.arg(line); + result += entryEnd; + } + return result; +} + QString CModListView::genModInfoText(CModEntry &mod) { QString prefix = "

    %1: "; // shared prefix QString lineTemplate = prefix + "%2

    "; - QString urlTemplate = prefix + "%2

    "; + QString urlTemplate = prefix + "%3

    "; QString textTemplate = prefix + "

    %2

    "; QString listTemplate = "

    %1: %2

    "; QString noteTemplate = "

    %1

    "; QString result; - result += ""; result += replaceIfNotEmpty(mod.getValue("name"), lineTemplate.arg(tr("Mod name"))); result += replaceIfNotEmpty(mod.getValue("installedVersion"), lineTemplate.arg(tr("Installed version"))); result += replaceIfNotEmpty(mod.getValue("latestVersion"), lineTemplate.arg(tr("Latest version"))); - if (mod.getValue("size").toDouble() != 0) + + if (mod.getValue("size").isValid()) result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("size").toDouble()), lineTemplate.arg(tr("Download size"))); result += replaceIfNotEmpty(mod.getValue("author"), lineTemplate.arg(tr("Authors"))); - result += replaceIfNotEmpty(mod.getValue("contact"), urlTemplate.arg(tr("Home"))); + + if (mod.getValue("licenseURL").isValid()) + result += urlTemplate.arg(tr("License")).arg(mod.getValue("licenseURL").toString()).arg(mod.getValue("licenseName").toString()); + + if (mod.getValue("contact").isValid()) + result += urlTemplate.arg(tr("Home")).arg(mod.getValue("contact").toString()).arg(mod.getValue("contact").toString()); + result += replaceIfNotEmpty(mod.getValue("depends"), lineTemplate.arg(tr("Required mods"))); result += replaceIfNotEmpty(mod.getValue("conflicts"), lineTemplate.arg(tr("Conflicting mods"))); result += replaceIfNotEmpty(mod.getValue("description"), textTemplate.arg(tr("Description"))); @@ -181,7 +218,6 @@ QString CModListView::genModInfoText(CModEntry &mod) if (notes.size()) result += textTemplate.arg(tr("Notes")).arg(notes); - result += ""; return result; } @@ -212,7 +248,8 @@ void CModListView::selectMod(const QModelIndex & index) { auto mod = modModel->getMod(index.data(ModRoles::ModNameRole).toString()); - ui->textBrowser->setHtml(genModInfoText(mod)); + ui->modInfoBrowser->setHtml(genModInfoText(mod)); + ui->changelogBrowser->setHtml(genChangelogText(mod)); bool hasInvalidDeps = !findInvalidDependencies(index.data(ModRoles::ModNameRole).toString()).empty(); bool hasBlockingMods = !findBlockingMods(index.data(ModRoles::ModNameRole).toString()).empty(); @@ -232,6 +269,8 @@ void CModListView::selectMod(const QModelIndex & index) ui->installButton->setEnabled(!hasInvalidDeps); ui->uninstallButton->setEnabled(!hasDependentMods); ui->updateButton->setEnabled(!hasInvalidDeps && !hasDependentMods); + + loadScreenshots(); } } @@ -239,7 +278,7 @@ void CModListView::keyPressEvent(QKeyEvent * event) { if (event->key() == Qt::Key_Escape && ui->modInfoWidget->isVisible() ) { - ui->modInfoWidget->hide(); + hideModInfo(); } else { @@ -480,17 +519,23 @@ void CModListView::hideProgressBar() void CModListView::installFiles(QStringList files) { QStringList mods; + QStringList images; // TODO: some better way to separate zip's with mods and downloaded repository files for (QString filename : files) { - if (filename.contains(".zip")) + if (filename.endsWith(".zip")) mods.push_back(filename); - if (filename.contains(".json")) + if (filename.endsWith(".json")) manager->loadRepository(filename); + if (filename.endsWith(".png")) + images.push_back(filename); } if (!mods.empty()) installMods(mods); + + if (!images.empty()) + loadScreenshots(); } void CModListView::installMods(QStringList archives) @@ -536,8 +581,23 @@ void CModListView::installMods(QStringList archives) for (int i=0; iinstallMod(modNames[i], archives[i]); + std::function enableMod = [&](QString modName) + { + auto mod = modModel->getMod(modName); + if (mod.isInstalled() && !mod.getValue("keepDisabled").toBool()) + { + if (manager->enableMod(modName)) + { + for (QString child : modModel->getChildren(modName)) + enableMod(child); + } + } + }; + for (QString mod : modsToEnable) - manager->enableMod(mod); + { + enableMod(mod); + } for (QString archive : archives) QFile::remove(archive); @@ -568,3 +628,50 @@ void CModListView::checkManagerErrors() QMessageBox::warning(this, title, description, QMessageBox::Ok, QMessageBox::Ok ); } } + +void CModListView::on_tabWidget_currentChanged(int index) +{ + loadScreenshots(); +} + +void CModListView::loadScreenshots() +{ + if (ui->tabWidget->currentIndex() == 2 && ui->modInfoWidget->isVisible()) + { + ui->screenshotsList->clear(); + QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString(); + assert(modModel->hasMod(modName)); //should be filtered out by check above + + for (QString & url : modModel->getMod(modName).getValue("screenshots").toStringList()) + { + // URL must be encoded to something else to get rid of symbols illegal in file names + auto hashed = QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Md5); + auto hashedStr = QString::fromUtf8(hashed.toHex()); + + QString fullPath = CLauncherDirs::get().downloadsPath() + '/' + hashedStr + ".png"; + QPixmap pixmap(fullPath); + if (pixmap.isNull()) + { + // image file not exists or corrupted - try to redownload + downloadFile(hashedStr + ".png", url, "screenshots"); + } + else + { + // managed to load cached image + QIcon icon(pixmap); + QListWidgetItem * item = new QListWidgetItem(icon, QString(tr("Screenshot %1")).arg(ui->screenshotsList->count() + 1)); + ui->screenshotsList->addItem(item); + } + } + } +} + +void CModListView::on_screenshotsList_clicked(const QModelIndex &index) +{ + if (index.isValid()) + { + QIcon icon = ui->screenshotsList->item(index.row())->icon(); + auto pixmap = icon.pixmap(icon.availableSizes()[0]); + ImageViewer::showPixmap(pixmap, this); + } +} diff --git a/launcher/modManager/cmodlistview_moc.h b/launcher/modManager/cmodlistview_moc.h index 332f469db..7803fc4d9 100644 --- a/launcher/modManager/cmodlistview_moc.h +++ b/launcher/modManager/cmodlistview_moc.h @@ -50,6 +50,7 @@ class CModListView : public QWidget void installMods(QStringList archives); void installFiles(QStringList mods); + QString genChangelogText(CModEntry & mod); QString genModInfoText(CModEntry & mod); public: explicit CModListView(QWidget *parent = 0); @@ -57,6 +58,7 @@ public: void showModInfo(); void hideModInfo(); + void loadScreenshots(); void enableModInfo(); void disableModInfo(); @@ -91,6 +93,10 @@ private slots: void on_allModsView_activated(const QModelIndex &index); + void on_tabWidget_currentChanged(int index); + + void on_screenshotsList_clicked(const QModelIndex &index); + private: Ui::CModListView *ui; }; diff --git a/launcher/modManager/cmodlistview_moc.ui b/launcher/modManager/cmodlistview_moc.ui index c9d9f74f2..d46cebe1b 100644 --- a/launcher/modManager/cmodlistview_moc.ui +++ b/launcher/modManager/cmodlistview_moc.ui @@ -202,50 +202,8 @@ 0 - - - - - 0 - 0 - - - - true - - - <!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; } -</style></head><body style=" font-family:'Ubuntu'; font-size:10pt; font-weight:400; font-style:normal;"> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:11pt;"><br /></p></body></html> - - - true - - - true - - - - - - - Qt::Horizontal - - - QSizePolicy::MinimumExpanding - - - - 0 - 20 - - - - - - + + 0 @@ -265,7 +223,7 @@ p, li { white-space: pre-wrap; } - Enable + Uninstall @@ -319,30 +277,21 @@ p, li { white-space: pre-wrap; }
    - - - - - 0 - 0 - + + + + Qt::Horizontal - + + QSizePolicy::MinimumExpanding + + - 51 - 0 + 0 + 20 - - - 100 - 16777215 - - - - Uninstall - - + @@ -369,6 +318,149 @@ p, li { white-space: pre-wrap; } + + + + + 0 + 0 + + + + + 51 + 0 + + + + + 100 + 16777215 + + + + Enable + + + + + + + 0 + + + + Description + + + + 4 + + + 4 + + + 4 + + + 4 + + + + + + 0 + 0 + + + + true + + + <!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; } +</style></head><body style=" font-family:'Ubuntu'; font-size:10pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:11pt;"><br /></p></body></html> + + + true + + + true + + + + + + + + Changelog + + + + 4 + + + 4 + + + 4 + + + 4 + + + + + + + + + Screenshots + + + + 4 + + + 4 + + + 4 + + + 4 + + + + + Qt::ScrollBarAlwaysOff + + + QAbstractItemView::NoSelection + + + QAbstractItemView::SelectRows + + + + 240 + 180 + + + + QListView::IconMode + + + true + + + + + + + @@ -390,6 +482,9 @@ p, li { white-space: pre-wrap; } + + 9 + @@ -437,7 +532,6 @@ p, li { white-space: pre-wrap; } lineEdit comboBox allModsView - textBrowser hideModInfoButton enableButton disableButton diff --git a/launcher/modManager/imageviewer.cpp b/launcher/modManager/imageviewer.cpp new file mode 100644 index 000000000..fe89a00a8 --- /dev/null +++ b/launcher/modManager/imageviewer.cpp @@ -0,0 +1,55 @@ +#include "StdInc.h" + +#include + +#include "imageviewer.h" +#include "ui_imageviewer.h" + +ImageViewer::ImageViewer(QWidget *parent) : + QDialog(parent), + ui(new Ui::ImageViewer) +{ + ui->setupUi(this); +} + +ImageViewer::~ImageViewer() +{ + delete ui; +} + +QSize ImageViewer::calculateWindowSize() +{ + QDesktopWidget desktop; + return desktop.availableGeometry(desktop.primaryScreen()).size() * 0.8; +} + +void ImageViewer::showPixmap(QPixmap & pixmap, QWidget *parent) +{ + assert(!pixmap.isNull()); + + ImageViewer * iw = new ImageViewer(parent); + + QSize size = pixmap.size(); + size.scale(iw->calculateWindowSize(), Qt::KeepAspectRatio); + iw->resize(size); + + iw->setPixmap(pixmap); + iw->setAttribute(Qt::WA_DeleteOnClose, true); + iw->setModal(Qt::WindowModal); + iw->show(); +} + +void ImageViewer::setPixmap(QPixmap & pixmap) +{ + ui->label->setPixmap(pixmap); +} + +void ImageViewer::mousePressEvent(QMouseEvent * event) +{ + close(); +} + +void ImageViewer::keyPressEvent(QKeyEvent * event) +{ + close(); // FIXME: it also closes on pressing modifiers (e.g. Ctrl/Alt). Not exactly expected +} diff --git a/launcher/modManager/imageviewer.h b/launcher/modManager/imageviewer.h new file mode 100644 index 000000000..a09ce9557 --- /dev/null +++ b/launcher/modManager/imageviewer.h @@ -0,0 +1,31 @@ +#ifndef IMAGEVIEWER_H +#define IMAGEVIEWER_H + +#include + +namespace Ui { + class ImageViewer; +} + +class ImageViewer : public QDialog +{ + Q_OBJECT + +public: + explicit ImageViewer(QWidget *parent = 0); + ~ImageViewer(); + + void setPixmap(QPixmap & pixmap); + + static void showPixmap(QPixmap & pixmap, QWidget *parent = 0); +protected: + void mousePressEvent(QMouseEvent * event); + void keyPressEvent(QKeyEvent * event); + QSize calculateWindowSize(); + + +private: + Ui::ImageViewer *ui; +}; + +#endif // IMAGEVIEWER_H diff --git a/launcher/modManager/imageviewer.ui b/launcher/modManager/imageviewer.ui new file mode 100644 index 000000000..52c26fc66 --- /dev/null +++ b/launcher/modManager/imageviewer.ui @@ -0,0 +1,58 @@ + + + ImageViewer + + + + 0 + 0 + 640 + 480 + + + + + 0 + 0 + + + + Image Viewer + + + + QLayout::SetNoConstraint + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + true + + + + + + + +