From 00e5968bd28ab1df33b3a39dbac8cda99aa2a0d2 Mon Sep 17 00:00:00 2001 From: Jan Dalheimer Date: Wed, 6 Apr 2016 23:09:30 +0200 Subject: [PATCH] NOISSUE Add a skeleton of the wonko system --- application/BuildConfig.cpp.in | 1 + application/BuildConfig.h | 5 + application/CMakeLists.txt | 9 + application/VersionProxyModel.cpp | 11 +- application/WonkoGui.cpp | 74 +++++ application/WonkoGui.h | 28 ++ application/dialogs/ProgressDialog.cpp | 12 + application/dialogs/ProgressDialog.h | 3 + application/pages/global/WonkoPage.cpp | 240 +++++++++++++++ application/pages/global/WonkoPage.h | 57 ++++ application/pages/global/WonkoPage.ui | 252 ++++++++++++++++ .../resources/multimc/16x16/looney.png | Bin 0 -> 802 bytes .../resources/multimc/256x256/looney.png | Bin 0 -> 68175 bytes .../resources/multimc/32x32/looney.png | Bin 0 -> 2147 bytes .../resources/multimc/64x64/looney.png | Bin 0 -> 6838 bytes application/resources/multimc/multimc.qrc | 6 + logic/BaseVersionList.cpp | 17 +- logic/BaseVersionList.h | 8 +- logic/CMakeLists.txt | 21 ++ logic/Env.cpp | 11 + logic/Env.h | 9 + logic/Json.h | 32 +- logic/java/JavaInstallList.cpp | 2 +- logic/java/JavaInstallList.h | 2 +- logic/minecraft/MinecraftVersionList.cpp | 2 +- logic/minecraft/MinecraftVersionList.h | 2 +- logic/minecraft/forge/ForgeVersionList.cpp | 2 +- logic/minecraft/forge/ForgeVersionList.h | 2 +- logic/wonko/BaseWonkoEntity.cpp | 39 +++ logic/wonko/BaseWonkoEntity.h | 51 ++++ logic/wonko/WonkoIndex.cpp | 147 +++++++++ logic/wonko/WonkoIndex.h | 68 +++++ logic/wonko/WonkoReference.cpp | 44 +++ logic/wonko/WonkoReference.h | 41 +++ logic/wonko/WonkoUtil.cpp | 47 +++ logic/wonko/WonkoUtil.h | 31 ++ logic/wonko/WonkoVersion.cpp | 102 +++++++ logic/wonko/WonkoVersion.h | 83 +++++ logic/wonko/WonkoVersionList.cpp | 283 ++++++++++++++++++ logic/wonko/WonkoVersionList.h | 92 ++++++ logic/wonko/format/WonkoFormat.cpp | 80 +++++ logic/wonko/format/WonkoFormat.h | 54 ++++ logic/wonko/format/WonkoFormatV1.cpp | 156 ++++++++++ logic/wonko/format/WonkoFormatV1.h | 30 ++ .../tasks/BaseWonkoEntityLocalLoadTask.cpp | 117 ++++++++ .../tasks/BaseWonkoEntityLocalLoadTask.h | 81 +++++ .../tasks/BaseWonkoEntityRemoteLoadTask.cpp | 126 ++++++++ .../tasks/BaseWonkoEntityRemoteLoadTask.h | 85 ++++++ tests/CMakeLists.txt | 4 + tests/tst_BaseWonkoEntityLocalLoadTask.cpp | 15 + tests/tst_BaseWonkoEntityRemoteLoadTask.cpp | 15 + tests/tst_WonkoIndex.cpp | 50 ++++ tests/tst_WonkoVersionList.cpp | 15 + 53 files changed, 2632 insertions(+), 32 deletions(-) create mode 100644 application/WonkoGui.cpp create mode 100644 application/WonkoGui.h create mode 100644 application/pages/global/WonkoPage.cpp create mode 100644 application/pages/global/WonkoPage.h create mode 100644 application/pages/global/WonkoPage.ui create mode 100644 application/resources/multimc/16x16/looney.png create mode 100644 application/resources/multimc/256x256/looney.png create mode 100644 application/resources/multimc/32x32/looney.png create mode 100644 application/resources/multimc/64x64/looney.png create mode 100644 logic/wonko/BaseWonkoEntity.cpp create mode 100644 logic/wonko/BaseWonkoEntity.h create mode 100644 logic/wonko/WonkoIndex.cpp create mode 100644 logic/wonko/WonkoIndex.h create mode 100644 logic/wonko/WonkoReference.cpp create mode 100644 logic/wonko/WonkoReference.h create mode 100644 logic/wonko/WonkoUtil.cpp create mode 100644 logic/wonko/WonkoUtil.h create mode 100644 logic/wonko/WonkoVersion.cpp create mode 100644 logic/wonko/WonkoVersion.h create mode 100644 logic/wonko/WonkoVersionList.cpp create mode 100644 logic/wonko/WonkoVersionList.h create mode 100644 logic/wonko/format/WonkoFormat.cpp create mode 100644 logic/wonko/format/WonkoFormat.h create mode 100644 logic/wonko/format/WonkoFormatV1.cpp create mode 100644 logic/wonko/format/WonkoFormatV1.h create mode 100644 logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.cpp create mode 100644 logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.h create mode 100644 logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.cpp create mode 100644 logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.h create mode 100644 tests/tst_BaseWonkoEntityLocalLoadTask.cpp create mode 100644 tests/tst_BaseWonkoEntityRemoteLoadTask.cpp create mode 100644 tests/tst_WonkoIndex.cpp create mode 100644 tests/tst_WonkoVersionList.cpp diff --git a/application/BuildConfig.cpp.in b/application/BuildConfig.cpp.in index be1797cb5..62bf53d76 100644 --- a/application/BuildConfig.cpp.in +++ b/application/BuildConfig.cpp.in @@ -32,6 +32,7 @@ Config::Config() VERSION_STR = "@MultiMC_VERSION_STRING@"; NEWS_RSS_URL = "@MultiMC_NEWS_RSS_URL@"; PASTE_EE_KEY = "@MultiMC_PASTE_EE_API_KEY@"; + WONKO_ROOT_URL = "@MultiMC_WONKO_ROOT_URL@"; } QString Config::printableVersionString() const diff --git a/application/BuildConfig.h b/application/BuildConfig.h index edba18e32..64d070656 100644 --- a/application/BuildConfig.h +++ b/application/BuildConfig.h @@ -57,6 +57,11 @@ public: */ QString PASTE_EE_KEY; + /** + * Root URL for wonko things. Other wonko URLs will be resolved relative to this. + */ + QString WONKO_ROOT_URL; + /** * \brief Converts the Version to a string. * \return The version number in string format (major.minor.revision.build). diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt index e3cadf741..5983fb428 100644 --- a/application/CMakeLists.txt +++ b/application/CMakeLists.txt @@ -26,6 +26,10 @@ set(MultiMC_PASTE_EE_API_KEY "" CACHE STRING "API key you can get from paste.ee #### Check the current Git commit and branch include(GetGitRevisionDescription) get_git_head_revision(MultiMC_GIT_REFSPEC MultiMC_GIT_COMMIT) + +# Root URL for wonko files +set(MultiMC_WONKO_ROOT_URL "" CACHE STRING "Root URL for wonko stuff") + message(STATUS "Git commit: ${MultiMC_GIT_COMMIT}") message(STATUS "Git refspec: ${MultiMC_GIT_REFSPEC}") @@ -99,6 +103,8 @@ SET(MULTIMC_SOURCES VersionProxyModel.cpp ColorCache.h ColorCache.cpp + WonkoGui.h + WonkoGui.cpp # GUI - windows MainWindow.h @@ -163,6 +169,8 @@ SET(MULTIMC_SOURCES pages/global/ProxyPage.h pages/global/PasteEEPage.cpp pages/global/PasteEEPage.h + pages/global/WonkoPage.cpp + pages/global/WonkoPage.h # GUI - dialogs dialogs/AboutDialog.cpp @@ -256,6 +264,7 @@ SET(MULTIMC_UIS pages/global/MultiMCPage.ui pages/global/ProxyPage.ui pages/global/PasteEEPage.ui + pages/global/WonkoPage.ui # Dialogs dialogs/CopyInstanceDialog.ui diff --git a/application/VersionProxyModel.cpp b/application/VersionProxyModel.cpp index 70894592a..22df7e09b 100644 --- a/application/VersionProxyModel.cpp +++ b/application/VersionProxyModel.cpp @@ -11,6 +11,8 @@ public: VersionFilterModel(VersionProxyModel *parent) : QSortFilterProxyModel(parent) { m_parent = parent; + setSortRole(BaseVersionList::SortRole); + sort(0, Qt::DescendingOrder); } bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const @@ -30,14 +32,11 @@ public: auto versionString = data.toString(); if(it.value().exact) { - if (versionString != it.value().string) - { - return false; - } + return versionString == it.value().string; } - else if (!versionIsInInterval(versionString, it.value().string)) + else { - return false; + return versionIsInInterval(versionString, it.value().string); } } default: diff --git a/application/WonkoGui.cpp b/application/WonkoGui.cpp new file mode 100644 index 000000000..4d376fdc6 --- /dev/null +++ b/application/WonkoGui.cpp @@ -0,0 +1,74 @@ +#include "WonkoGui.h" + +#include "dialogs/ProgressDialog.h" +#include "wonko/WonkoIndex.h" +#include "wonko/WonkoVersionList.h" +#include "wonko/WonkoVersion.h" +#include "Env.h" + +WonkoIndexPtr Wonko::ensureIndexLoaded(QWidget *parent) +{ + if (!ENV.wonkoIndex()->isLocalLoaded()) + { + ProgressDialog(parent).execWithTask(ENV.wonkoIndex()->localUpdateTask()); + if (!ENV.wonkoIndex()->isRemoteLoaded() && ENV.wonkoIndex()->lists().size() == 0) + { + ProgressDialog(parent).execWithTask(ENV.wonkoIndex()->remoteUpdateTask()); + } + } + return ENV.wonkoIndex(); +} + +WonkoVersionListPtr Wonko::ensureVersionListExists(const QString &uid, QWidget *parent) +{ + ensureIndexLoaded(parent); + if (!ENV.wonkoIndex()->isRemoteLoaded() && !ENV.wonkoIndex()->hasUid(uid)) + { + ProgressDialog(parent).execWithTask(ENV.wonkoIndex()->remoteUpdateTask()); + } + return ENV.wonkoIndex()->getList(uid); +} +WonkoVersionListPtr Wonko::ensureVersionListLoaded(const QString &uid, QWidget *parent) +{ + WonkoVersionListPtr list = ensureVersionListExists(uid, parent); + if (!list) + { + return nullptr; + } + if (!list->isLocalLoaded()) + { + ProgressDialog(parent).execWithTask(list->localUpdateTask()); + if (!list->isLocalLoaded()) + { + ProgressDialog(parent).execWithTask(list->remoteUpdateTask()); + } + } + return list->isComplete() ? list : nullptr; +} + +WonkoVersionPtr Wonko::ensureVersionExists(const QString &uid, const QString &version, QWidget *parent) +{ + WonkoVersionListPtr list = ensureVersionListLoaded(uid, parent); + if (!list) + { + return nullptr; + } + return list->getVersion(version); +} +WonkoVersionPtr Wonko::ensureVersionLoaded(const QString &uid, const QString &version, QWidget *parent, const UpdateType update) +{ + WonkoVersionPtr vptr = ensureVersionExists(uid, version, parent); + if (!vptr) + { + return nullptr; + } + if (!vptr->isLocalLoaded() || update == AlwaysUpdate) + { + ProgressDialog(parent).execWithTask(vptr->localUpdateTask()); + if (!vptr->isLocalLoaded() || update == AlwaysUpdate) + { + ProgressDialog(parent).execWithTask(vptr->remoteUpdateTask()); + } + } + return vptr->isComplete() ? vptr : nullptr; +} diff --git a/application/WonkoGui.h b/application/WonkoGui.h new file mode 100644 index 000000000..2b87b819a --- /dev/null +++ b/application/WonkoGui.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +class QWidget; +class QString; + +using WonkoIndexPtr = std::shared_ptr; +using WonkoVersionListPtr = std::shared_ptr; +using WonkoVersionPtr = std::shared_ptr; + +namespace Wonko +{ +enum UpdateType +{ + AlwaysUpdate, + UpdateIfNeeded +}; + +/// Ensures that the index has been loaded, either from the local cache or remotely +WonkoIndexPtr ensureIndexLoaded(QWidget *parent); +/// Ensures that the given uid exists. Returns a nullptr if it doesn't. +WonkoVersionListPtr ensureVersionListExists(const QString &uid, QWidget *parent); +/// Ensures that the given uid exists and is loaded, either from the local cache or remotely. Returns nullptr if it doesn't exist or couldn't be loaded. +WonkoVersionListPtr ensureVersionListLoaded(const QString &uid, QWidget *parent); +WonkoVersionPtr ensureVersionExists(const QString &uid, const QString &version, QWidget *parent); +WonkoVersionPtr ensureVersionLoaded(const QString &uid, const QString &version, QWidget *parent, const UpdateType update = UpdateIfNeeded); +} diff --git a/application/dialogs/ProgressDialog.cpp b/application/dialogs/ProgressDialog.cpp index 17ab79cd4..5d7c7968b 100644 --- a/application/dialogs/ProgressDialog.cpp +++ b/application/dialogs/ProgressDialog.cpp @@ -97,6 +97,18 @@ int ProgressDialog::execWithTask(Task *task) } } +// TODO: only provide the unique_ptr overloads +int ProgressDialog::execWithTask(std::unique_ptr &&task) +{ + connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); + return execWithTask(task.release()); +} +int ProgressDialog::execWithTask(std::unique_ptr &task) +{ + connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); + return execWithTask(task.release()); +} + bool ProgressDialog::handleImmediateResult(QDialog::DialogCode &result) { if(task->isFinished()) diff --git a/application/dialogs/ProgressDialog.h b/application/dialogs/ProgressDialog.h index 9ddbceb1d..28d4e6398 100644 --- a/application/dialogs/ProgressDialog.h +++ b/application/dialogs/ProgressDialog.h @@ -16,6 +16,7 @@ #pragma once #include +#include class Task; @@ -35,6 +36,8 @@ public: void updateSize(); int execWithTask(Task *task); + int execWithTask(std::unique_ptr &&task); + int execWithTask(std::unique_ptr &task); void setSkipButton(bool present, QString label = QString()); diff --git a/application/pages/global/WonkoPage.cpp b/application/pages/global/WonkoPage.cpp new file mode 100644 index 000000000..21de23470 --- /dev/null +++ b/application/pages/global/WonkoPage.cpp @@ -0,0 +1,240 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WonkoPage.h" +#include "ui_WonkoPage.h" + +#include +#include +#include + +#include "dialogs/ProgressDialog.h" +#include "VersionProxyModel.h" + +#include "wonko/WonkoIndex.h" +#include "wonko/WonkoVersionList.h" +#include "wonko/WonkoVersion.h" +#include "Env.h" +#include "MultiMC.h" + +static QString formatRequires(const WonkoVersionPtr &version) +{ + QStringList lines; + for (const WonkoReference &ref : version->requires()) + { + const QString readable = ENV.wonkoIndex()->hasUid(ref.uid()) ? ENV.wonkoIndex()->getList(ref.uid())->humanReadable() : ref.uid(); + if (ref.version().isEmpty()) + { + lines.append(readable); + } + else + { + lines.append(QString("%1 (%2)").arg(readable, ref.version())); + } + } + return lines.join('\n'); +} + +WonkoPage::WonkoPage(QWidget *parent) : + QWidget(parent), + ui(new Ui::WonkoPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + + m_fileProxy = new QSortFilterProxyModel(this); + m_fileProxy->setSortRole(Qt::DisplayRole); + m_fileProxy->setSortCaseSensitivity(Qt::CaseInsensitive); + m_fileProxy->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_fileProxy->setFilterRole(Qt::DisplayRole); + m_fileProxy->setFilterKeyColumn(0); + m_fileProxy->sort(0); + m_fileProxy->setSourceModel(ENV.wonkoIndex().get()); + ui->indexView->setModel(m_fileProxy); + + m_filterProxy = new QSortFilterProxyModel(this); + m_filterProxy->setSortRole(WonkoVersionList::SortRole); + m_filterProxy->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_filterProxy->setFilterRole(Qt::DisplayRole); + m_filterProxy->setFilterKeyColumn(0); + m_filterProxy->sort(0, Qt::DescendingOrder); + ui->versionsView->setModel(m_filterProxy); + + m_versionProxy = new VersionProxyModel(this); + m_filterProxy->setSourceModel(m_versionProxy); + + connect(ui->indexView->selectionModel(), &QItemSelectionModel::currentChanged, this, &WonkoPage::updateCurrentVersionList); + connect(ui->versionsView->selectionModel(), &QItemSelectionModel::currentChanged, this, &WonkoPage::updateVersion); + connect(m_filterProxy, &QSortFilterProxyModel::dataChanged, this, &WonkoPage::versionListDataChanged); + + updateCurrentVersionList(QModelIndex()); + updateVersion(); +} + +WonkoPage::~WonkoPage() +{ + delete ui; +} + +QIcon WonkoPage::icon() const +{ + return MMC->getThemedIcon("looney"); +} + +void WonkoPage::on_refreshIndexBtn_clicked() +{ + ProgressDialog(this).execWithTask(ENV.wonkoIndex()->remoteUpdateTask()); +} +void WonkoPage::on_refreshFileBtn_clicked() +{ + WonkoVersionListPtr list = ui->indexView->currentIndex().data(WonkoIndex::ListPtrRole).value(); + if (!list) + { + return; + } + ProgressDialog(this).execWithTask(list->remoteUpdateTask()); +} +void WonkoPage::on_refreshVersionBtn_clicked() +{ + WonkoVersionPtr version = ui->versionsView->currentIndex().data(WonkoVersionList::WonkoVersionPtrRole).value(); + if (!version) + { + return; + } + ProgressDialog(this).execWithTask(version->remoteUpdateTask()); +} + +void WonkoPage::on_fileSearchEdit_textChanged(const QString &search) +{ + if (search.isEmpty()) + { + m_fileProxy->setFilterFixedString(QString()); + } + else + { + QStringList parts = search.split(' '); + std::transform(parts.begin(), parts.end(), parts.begin(), &QRegularExpression::escape); + m_fileProxy->setFilterRegExp(".*" + parts.join(".*") + ".*"); + } +} +void WonkoPage::on_versionSearchEdit_textChanged(const QString &search) +{ + if (search.isEmpty()) + { + m_filterProxy->setFilterFixedString(QString()); + } + else + { + QStringList parts = search.split(' '); + std::transform(parts.begin(), parts.end(), parts.begin(), &QRegularExpression::escape); + m_filterProxy->setFilterRegExp(".*" + parts.join(".*") + ".*"); + } +} + +void WonkoPage::updateCurrentVersionList(const QModelIndex &index) +{ + if (index.isValid()) + { + WonkoVersionListPtr list = index.data(WonkoIndex::ListPtrRole).value(); + ui->versionsBox->setEnabled(true); + ui->refreshFileBtn->setEnabled(true); + ui->fileUidLabel->setEnabled(true); + ui->fileUid->setText(list->uid()); + ui->fileNameLabel->setEnabled(true); + ui->fileName->setText(list->name()); + m_versionProxy->setSourceModel(list.get()); + ui->refreshFileBtn->setText(tr("Refresh %1").arg(list->humanReadable())); + + if (!list->isLocalLoaded()) + { + std::unique_ptr task = list->localUpdateTask(); + connect(task.get(), &Task::finished, this, [this, list]() + { + if (list->count() == 0 && !list->isRemoteLoaded()) + { + ProgressDialog(this).execWithTask(list->remoteUpdateTask()); + } + }); + ProgressDialog(this).execWithTask(task); + } + } + else + { + ui->versionsBox->setEnabled(false); + ui->refreshFileBtn->setEnabled(false); + ui->fileUidLabel->setEnabled(false); + ui->fileUid->clear(); + ui->fileNameLabel->setEnabled(false); + ui->fileName->clear(); + m_versionProxy->setSourceModel(nullptr); + ui->refreshFileBtn->setText(tr("Refresh ___")); + } +} + +void WonkoPage::versionListDataChanged(const QModelIndex &tl, const QModelIndex &br) +{ + if (QItemSelection(tl, br).contains(ui->versionsView->currentIndex())) + { + updateVersion(); + } +} + +void WonkoPage::updateVersion() +{ + WonkoVersionPtr version = std::dynamic_pointer_cast( + ui->versionsView->currentIndex().data(WonkoVersionList::VersionPointerRole).value()); + if (version) + { + ui->refreshVersionBtn->setEnabled(true); + ui->versionVersionLabel->setEnabled(true); + ui->versionVersion->setText(version->version()); + ui->versionTimeLabel->setEnabled(true); + ui->versionTime->setText(version->time().toString("yyyy-MM-dd HH:mm")); + ui->versionTypeLabel->setEnabled(true); + ui->versionType->setText(version->type()); + ui->versionRequiresLabel->setEnabled(true); + ui->versionRequires->setText(formatRequires(version)); + ui->refreshVersionBtn->setText(tr("Refresh %1").arg(version->version())); + } + else + { + ui->refreshVersionBtn->setEnabled(false); + ui->versionVersionLabel->setEnabled(false); + ui->versionVersion->clear(); + ui->versionTimeLabel->setEnabled(false); + ui->versionTime->clear(); + ui->versionTypeLabel->setEnabled(false); + ui->versionType->clear(); + ui->versionRequiresLabel->setEnabled(false); + ui->versionRequires->clear(); + ui->refreshVersionBtn->setText(tr("Refresh ___")); + } +} + +void WonkoPage::opened() +{ + if (!ENV.wonkoIndex()->isLocalLoaded()) + { + std::unique_ptr task = ENV.wonkoIndex()->localUpdateTask(); + connect(task.get(), &Task::finished, this, [this]() + { + if (!ENV.wonkoIndex()->isRemoteLoaded()) + { + ProgressDialog(this).execWithTask(ENV.wonkoIndex()->remoteUpdateTask()); + } + }); + ProgressDialog(this).execWithTask(task); + } +} diff --git a/application/pages/global/WonkoPage.h b/application/pages/global/WonkoPage.h new file mode 100644 index 000000000..fd77ee0d8 --- /dev/null +++ b/application/pages/global/WonkoPage.h @@ -0,0 +1,57 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "pages/BasePage.h" + +namespace Ui { +class WonkoPage; +} + +class QSortFilterProxyModel; +class VersionProxyModel; + +class WonkoPage : public QWidget, public BasePage +{ + Q_OBJECT +public: + explicit WonkoPage(QWidget *parent = 0); + ~WonkoPage(); + + QString id() const override { return "wonko-global"; } + QString displayName() const override { return tr("Wonko"); } + QIcon icon() const override; + void opened() override; + +private slots: + void on_refreshIndexBtn_clicked(); + void on_refreshFileBtn_clicked(); + void on_refreshVersionBtn_clicked(); + void on_fileSearchEdit_textChanged(const QString &search); + void on_versionSearchEdit_textChanged(const QString &search); + void updateCurrentVersionList(const QModelIndex &index); + void versionListDataChanged(const QModelIndex &tl, const QModelIndex &br); + +private: + Ui::WonkoPage *ui; + QSortFilterProxyModel *m_fileProxy; + QSortFilterProxyModel *m_filterProxy; + VersionProxyModel *m_versionProxy; + + void updateVersion(); +}; diff --git a/application/pages/global/WonkoPage.ui b/application/pages/global/WonkoPage.ui new file mode 100644 index 000000000..2d14cecae --- /dev/null +++ b/application/pages/global/WonkoPage.ui @@ -0,0 +1,252 @@ + + + WonkoPage + + + + 0 + 0 + 640 + 480 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + Tab 1 + + + + + + Versions + + + + + + Search... + + + true + + + + + + + true + + + false + + + + + + + + + Refresh ___ + + + + + + + + + + + Version: + + + + + + + + + + + + + + Time: + + + + + + + + + + + + + + Type: + + + + + + + + + + + + + + Dependencies: + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Resources + + + + + + Search... + + + true + + + + + + + true + + + false + + + + + + + + + Refresh ___ + + + + + + + + + + + UID: + + + + + + + + + + + + + + Name: + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Refresh Index + + + + + + + + + + + + diff --git a/application/resources/multimc/16x16/looney.png b/application/resources/multimc/16x16/looney.png new file mode 100644 index 0000000000000000000000000000000000000000..ea0d7c18f8a883238d820fb4cab19ec9979e0a79 GIT binary patch literal 802 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7Sc;uILpV4%IBGajIv5xj zI14-?iy0XB4ude`@%$Aj3=B+1JY5_^EP6{P`FBW{iX69pZ$8;GZBovrlWLJ?Pn9j5 z`qnGDaI0JwTNk@4%ifCB)1AIvxaYM}L(YXo^IM3<6r~RJo0B^GRA#t#00~oL9g4`p^IVu&-bJO;EnBuFSgBwf-v2Q~Qs7U%S)2{>$n9 z`d|01XP3*RJ$-m=*HQNwp9ySIHlIIav!{?xox4w)K`n|32VFG`@5=^_rMKJizh1Uy>3(=6;Kp2 z5OtB-!se{Ndg$Ycpocp)?fLX+yZ!xtxX^?2clE+@@}`c1Q) zniAL+U)N5Z#;}cb&5vTPy~l+FT8@~VJS)B-i>EoWY>g`hx1O#!|ta&J4 z-z9nf*VML&3%FcgaUF76S^Mz&Zt>+T_t*R{v^7XS_m%gk+nNtY6INeOUC~`}YvJ>+ zFM6DJNh!^})iBRaDPg+Q_Kr76H>%UL=uWYe#*QJ+}2yw&Fd=SdZ(5H^>EypvrXm;QcdEg$pi4by`EtQ&%4sxN;qo5jGu Oz~JfX=d#Wzp$P!iuY?u= literal 0 HcmV?d00001 diff --git a/application/resources/multimc/256x256/looney.png b/application/resources/multimc/256x256/looney.png new file mode 100644 index 0000000000000000000000000000000000000000..ac48231b9e889ecfd654c6b7dd3b91474060f93e GIT binary patch literal 68175 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⁡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 VI(>g)IRgU&gQu&X%Q~loCIG!<`(ywB literal 0 HcmV?d00001 diff --git a/application/resources/multimc/64x64/looney.png b/application/resources/multimc/64x64/looney.png new file mode 100644 index 0000000000000000000000000000000000000000..fbdbb8563b232e0ca4ad827122af85dfd8cc0668 GIT binary patch literal 6838 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hEe(&L(;&XCgb}>7~dw|RA4ZcTi^|DW|gz3nT1b)Vn!mp8wva$a1`$z!J1 z4Xt*4ZIu7}v%K!%>crVGXQ%(a5q&SHb!p{qh2`(J?C!sRasDr++vc&yb8?-8Yd3G) z_))z5^3D8RWtQ7cKRf<-=J{O~5y>ULPurX6JW;)0e!uo%)-Cq%i4Eqq|G!<&{Z(*( z*?PYt&CYI#{de?Fgk?H874oWR@Bi@N>ZbSI zuU83t$UR?u^6u-qKkr+K>0hx-Gf`QSHmNaAH1Gn?J!vn&W9J?eyB$%INDC_Xvu*l5 z{oHvL^@-{K)~?jB-?`KB=RElzQ_JnEmgeqV#Ion5X!+eR-RbLZpLn>t-*10G_xrLL z%X_36SppR}7Dz4*kXUHsv~(tugH9N0;EAgpA$%)hUEjz)JkeSF?#D;@UkC61`+9%J zB(r!yrn)!htnc1GbLp<@?*FH+Un$d;c^7o%`--K@O<6M*Y1y>9U{W9yDVGPuy@ILQz_AVTTqocdMaFs8O4p+OH`ui`5oCnd7<2B7A~~Qrm(Bt9WEWm{OL8 zv!-YMa#0s6^GXAQ zH9_+@jx`7$)T|Ko64ZKsa@I*E$0-`x%G`n{E-XIdwCt=x*a;@t4QIL(7mA<1G~fF3 zulTClxBpx|Sa{R%!?DH3>$gS4-BD0;(P=GS!gE<-X6jL~wp-R*Jd=#r4zKvc!{sb& z)XnhdNoUVOj`g zzUJ%oeIfCG4~f@)YUa0}Gmq!^)8&qf=Vu#stPO}R+R-EPt#hZvwAR#_#)|@yS6p2X z8BsDZa(>3<33F7LYR^PG*I3omaOr7F@wCRRKXjI`qzLf5l8rnlq4?YUS9uWdAmQ`<4sKuX2<{hHTieC{ohBD)6~khNC|33 zWE-ZaIEpB?G;rv!Rx$Gm9LhQ`ZW(Ch&N=1YMvg|Ou&oV(hgWQ9No9F`V$qvp9$o?6 z>XTmvspvk7IJu%RXzH#l5$PVM&nz&=xwGS_=KFn#bw6%!H}Kcqd|$Eh!TxvGZr|To z{BH96;XMs5CbG5nkEc#d z^KW}TDe1uL_;0_WQ#bz3&OZCtXjy8+x~_Q>wO0nVDT(Yg5|N1VZk%+);Or@~FRrkq^lWrL)p*%Brk`$i1r1Gk1Uz75fc_?a))48tF zc^*kwY%fxNyVrf%yZ_hyc|4a@a~nHlXT zDoBiP{?vuxS&fgK{T=GF~n7UT35Kk4uw&`E+=_ZAqM|(VJR7bH-{TqeC7# zLdI+_f|XWIxZPo{J!7@(f@(p%Im+aG>DzVp3{GP{f5(f>tB>IYOv4BxU$A8 zC`@yu-^`6)S07{NTAJ#&NRKBv`S=pEiu4by%jLv+*i`Rsa&?_qHdCW%bK51ZuLlh8 zeCXl&;Fh_#NKtFQrB`YSOUq(2zZ4b66>5lh!tkmsdA<7@U{9=rd6$Y|16Btb-D+9#dvrRNMLM#!2z| z&wJPZIcxvt!r_ly0v`@dKEF5i+2`;Op_jp@gM`?Y`W-3_J?j?M6v}sYRl`ha3xUKu zg{LjEeDq{?YIP_n%=~(?Au%?{vb}TWAqS3`sVzIXelzmjK2sujP;!sOGDD@-#JiE# zyxPC_*|A^Y>noBqPxSUH-tBX2e)6iv|Jl0#`-j8vUPn}qHZ*Jf>8^^Djm`QQ z>;2KCQzUZ5rL6p2hc;~Q(P=QAe2<`Q_w0 zeyDkuJb&rjcg7MDT}21Zi0SM-+}2Sd8@qc6Z{r#V{f?APDznP2dcO!MbTjY^m>(G( zmr!wQZ$a&bZ->?jtIM&~J(3jsaFD;wfa(6{W9jzqQ*QgcKcnK)*I_>Ay|l}enayFX zaXj~;ygGf{Sk3MKv&MX1vp#X6Xmn_f%ELp;N_Ow(g1`Q=Q-96WXMeUy>tNEBf7iBIw!5zM>VI9m z>eQw5)O7+Og6pTq_4Rt^SryKiyZQH=dDCj@)Qdko*?MP7t?*QhuBEr{Csq~AT3|5S zT&DWm{iD5RGq>@cKKD7p_|cb}%yEU6&ptUWe~@wWI>mN}LsH5yFDMzz1ood-e}SXHDx81vrkz38fR?en0#rYt$<(7 z6^~aMJq8+DVw>BV@7BuwJ|uo(qk6gS7TtaPPkuJ#T{Mtb->6;qR3Mx$pmEW<8~Yx* zeQ*ktiaC|DYm%vbLK^Q}W1U-aCnZl#D0$kNRr~LZ`uAVzhl@`xKj_Xc-+MdsYib6EYPw$SkWyt zbHVnRGm0y36g_%e{rmlXB^HJ`E8351zm2z^(&5>~Y?9v8^i@vz)UgwWk`b%6N~upf zx+wYe53}aNv$r-JT0ZaZaXVi@u_YHCaP{wKierAYV8!tS?^TJ8+i#@b-FD)hWY<%X z^|o3wKS$nlRA}(zd>-yPafZ(Zr{!&5A2#gEkN@#z^7-2D@$+qO|M?%Gr^)tbasJP5 z;=HCZ)lY=$KYrR|yWmkLpFvk)Uuwsf-1evr%cNY^Yet*qEwX;{;d8z3nqv|l1(+V6 z+WbPiB7KFLv_$a5x9YRH(|7*3Uf+1v!qIk}M)L80M~_P`ZF`%&y(9PV&$+ce97`CE z_vIyg+@oixf8do;oQu zEj02@jLq2=zDzwM>)M7zF~Q4<(=O@t8J6L$`)P4=B}ej2A-ORBCd|>C){1tK7H3T z5#}9?se2DrY;nCEy{PJz>+ub8%@V&PPNpPz^|U*uS*NdD^7Po+WSRKe5w_vI9gj{v z^59TA9HH~ta8qw;@6|+&i5DJf%zH-VHMYcQgU>==V);GYh3?$eBIx5 z^}o~Q>-FE*sDkYN|J48ffAy2Q>T5QBubd~N==@MOlG*mc#1~r@Z(Y6ZO5JLiq=+x8 zD;Jui`!oQJRTt}vF7xV3&EmdzOuf>9TKYFWDV1j z_Ahz4JM^sM>BKjdvzdNt#L7#qlU3~NzyGnMAT~(s$Cr2Kj>p#=)@6QOk;YEN6Q%cFpk& z>#)51?)@U^b9SFz`tK3^aY9t}!M^^w1v{Tld;VWL*?s@r@49;hgdx(!2Z)tJ=SePV+6WoEeKRI?p6O?oBZ zslCwEOlR_?Os2`2F`Y9UjAn|Z8r@3CO`g5-Qk|;J^s8d>yz72<=x;yADtjq1HBWi> zy@IYITei5c*9P6wa z)U|cHXGR9u{rq`(j+Oc1qZ54`&&|uQJ^kW^$PwvZDn7eil6f_?{ibZ%5MxpP%YR+= zulc*QSoXaC|L5T2gTKW?rK2xjn!>YZsg}(;4aUR5XARO$I|xtFON%nvCbz-P@|f)Q zy$x?qT5UGo{e@lj^WC<7n=j#8PWKp^p5qhLmAslfuk>|s#C?k{ZLMXO)23D~5;FGp znJa4?TDAF>>Q;$ghna(_f1Eg&V)V-^La*9lSK;G}TmL2XWk0hxT`)0jikapiuUCt% zT}b0d3KZmZHxQI#WC^QgNEKs&t}c`Pg;}U zsWYwE>D`8F7g`yTJR>`lwxqCeEEQ#OerRKO`N zweI?O-iXzBBonxBOIuuZRp*za%txzsXzod_|E^@jHDgY|$r%R)qAjnedA-&+`e2Jf zs8^1#l=u>v2%qmaZ{AJ}xPIoD_q4lOA2iuS9XG6=oEi63Xt%(Nwdu7x{)ujk5o7#( zRIL}~-2L;%{*ui9XA5TDH~tWF|NUIm6#_|VH`$$p-1aSrG%-1rs1h8` z8dn{Yb#Y0?8IzUKyf#HD%h-HnAI$XsZn$WPM8<7Z-|K>#9eY?s)Y*#q*K2*NR-K+~ zwtanN#^+<{|MP!52u|9$=5oTu-DMm0EnNKOud~stKc953&;Rk<{(C!rT|({K6X~~~ zK9YYTl5=iDpj6wJkX2LmskL!fCYG4K_p{s-bH=0AJ8Sxky1pZ<3ug-^`#WXqI&kn$ z!=cD&-P2y?oP2q>?c6kr)*vQjaY?D>X~L_d|DBWfWslCcd)#LKQ+of`=+En0o_2Ao z&M$wvb~}Ijo>gzl;!7V3*1w;1rtt5iiy1v>7i1)^iI&C&imN`#UiHu_N&5x!5r^xh zUuwPOnFAMQK2P91t@ruFxt?xcr&!0i%*jU5&0O-L+spnOHjn+kC;!h!{+b8geS70g znfC3-+gFOrGr$YjTrL3r5?O zQJu%-Gm*y{M3-{TyqkJ{@o(L^5_>ofPm$QI6dC>JUBcfRYby)BoR|CYx&2<9W|^Jx z)EmLN>F)YEYnK+S7EZd|e)ZnfGc`AhZk(BseOT$tizH_r-r7?xGK$=as?%cKI~v!$ z<}|#l=DOC=B(Z4Gsw^|J{@N$y`wKoC>rLPL#dd9E^(Dm@lW$d;9$;2E)Yhf={J;Xe zj#VYG46&Y%zVVBz>^!g}f8}41K*!vzLKa`V(>62z+NZc;l2Ffa=a}1p9)}p>bTX6{ z=>OVh{%5cKz8$;GuP*(+&3o(TRe}je)~t4$BkUFY$k%ZN)9HQ6M?7x3e$$h?Aamx! zv7{pk_tu~LywN?`#iY43WO3;ncIeuWFk=Yum@~XD(hmU;m)qZo{4s z>-AEM*TVI;FFR=Moa?=aw`8xOOn7 zRbKv9y8PY$x%VwA)XL&)`3>f1%{jb)=UDQu98I=)8e%6o4Oz3BOJ6;-I>sMR_~q=B zw6X(l3i?xYj;&GAXM1tAL4={^Me=-0-Z^dhxkYiWc4e{1NdIbYUwV8x)4n6Yii!pf z8M7_pcsi8Ry3JK@igXCCd1+WFo*XXKv`A}$gkpMC{mn(&)MLY28@AV7Hn-hxYx7-s zslMQAjn%%pBsM-ep>@UCy`ymk^TxRg&n3nzihOgq+&Cst^R?pDf`%@|mT4~D@8^}A zslWNUOZdaB$;lM>>AHaA&S*p68Clx{gYXUg-8H!HN- zrzF?d9+}1*6Zq)Rq^AcfP4w1P+$}n)b#VKhclRE<7JpR#^yrGM^Y$C7R-LIRK9un} z>PVhaL=oST3zMDwPf5m4tB6(QSxq`BL?bqb%uhv)m zZ=POb_~_&{W3>ml>-Uz+R{#5b@q<6xCX29R&RNS>oV}7Q&7*c*A+N`SG4iD0B|=f5s*Lc$5orF@Bi<#|Ixhs z%Kw;9rh7%_Z6_POxV`t4eeCMp$&VLK`lYGnzQ?en@MOXkN6R@0F|OhZw6u7f_UD|` zyi~4x_@cyP8^u(o`xA>6NnN>dX#J`~BAj<+4oy1zms_LYhUBLozb>!4_ouJ?ZvB7x z`cTdv-*&%`E8h9zV4`Av-1<+S)wTv$>=tre_MyeW>f-ixk*gCH$t2abMzKroDPAJr zskmU_;pw8DirtEB^A>3oJk8p6y?x#sWlPqgtjKfAd1{=FZk}Rz`JBJt|Hb{R|L!vL z+k9A}{6X98Ky+O3_50;Z{~kRXSeg?wmB*`Fw`I`^PtT0sa)w+ix1LQnH2ue-MITg8 zuKPFDc-n>UE$fbEvt@I5p5#gmOqzQ*!uv$$yUtSqDW%3L$NCO6oANzoJCZX~PwBV! z@8xwxPh{WMeBZt9+|rfidw*?RFJ1Zn+u6oDd-sL?sIpk_&H2dGlz63QoeE+?$5@Ku zF6l?l*;?`ZK`A9N583#w7>nsN1m>9Urper>Kj zg%!aQf)`|p&zb9)v-QWjcKiP`|9@$j_Hw51hiB{m|1z(Zu1m045m#1{n{$m_HPp7H zs%axzvW`NE?~^$vEt?(AO}wD*X&89j^jhSa#wkk2W=vhRQDaIe*gPxz5UmK4=Xu- zY_op1W81o~;;$~<*}Tzi+Q9^mNZysN6t}H7x$Qvp|Lt)}9flis$tf<8IdN-pP)zU9 zHLekYPv$tg1jH`gJI7cvy@%tNScgPX&k>2jdk?#rLQimpIJT;vFpt>1if40E$5QUb zpNkUHue~{Y>%(L3_wRTA|Mz=-k;SL~0tMmdKI;Vst0D_w_qyPW_ literal 0 HcmV?d00001 diff --git a/application/resources/multimc/multimc.qrc b/application/resources/multimc/multimc.qrc index 6a6250803..3f7c5e8fa 100644 --- a/application/resources/multimc/multimc.qrc +++ b/application/resources/multimc/multimc.qrc @@ -236,5 +236,11 @@ scalable/discord.svg + + + 16x16/looney.png + 32x32/looney.png + 64x64/looney.png + 256x256/looney.png diff --git a/logic/BaseVersionList.cpp b/logic/BaseVersionList.cpp index 73f4a7ef1..b34f318c3 100644 --- a/logic/BaseVersionList.cpp +++ b/logic/BaseVersionList.cpp @@ -72,7 +72,7 @@ QVariant BaseVersionList::data(const QModelIndex &index, int role) const } } -BaseVersionList::RoleList BaseVersionList::providesRoles() +BaseVersionList::RoleList BaseVersionList::providesRoles() const { return {VersionPointerRole, VersionRole, VersionIdRole, TypeRole}; } @@ -87,3 +87,18 @@ int BaseVersionList::columnCount(const QModelIndex &parent) const { return 1; } + +QHash BaseVersionList::roleNames() const +{ + QHash roles = QAbstractListModel::roleNames(); + roles.insert(VersionRole, "version"); + roles.insert(VersionIdRole, "versionId"); + roles.insert(ParentGameVersionRole, "parentGameVersion"); + roles.insert(RecommendedRole, "recommended"); + roles.insert(LatestRole, "latest"); + roles.insert(TypeRole, "type"); + roles.insert(BranchRole, "branch"); + roles.insert(PathRole, "path"); + roles.insert(ArchitectureRole, "architecture"); + return roles; +} diff --git a/logic/BaseVersionList.h b/logic/BaseVersionList.h index 42ea77c0d..73d2ee1f2 100644 --- a/logic/BaseVersionList.h +++ b/logic/BaseVersionList.h @@ -50,9 +50,10 @@ public: TypeRole, BranchRole, PathRole, - ArchitectureRole + ArchitectureRole, + SortRole }; - typedef QList RoleList; + typedef QList RoleList; explicit BaseVersionList(QObject *parent = 0); @@ -78,9 +79,10 @@ public: virtual QVariant data(const QModelIndex &index, int role) const; virtual int rowCount(const QModelIndex &parent) const; virtual int columnCount(const QModelIndex &parent) const; + virtual QHash roleNames() const override; //! which roles are provided by this version list? - virtual RoleList providesRoles(); + virtual RoleList providesRoles() const; /*! * \brief Finds a version by its descriptor. diff --git a/logic/CMakeLists.txt b/logic/CMakeLists.txt index 19236e1bf..cd8aa2461 100644 --- a/logic/CMakeLists.txt +++ b/logic/CMakeLists.txt @@ -313,6 +313,27 @@ set(LOGIC_SOURCES tools/MCEditTool.cpp tools/MCEditTool.h + # Wonko + wonko/tasks/BaseWonkoEntityRemoteLoadTask.cpp + wonko/tasks/BaseWonkoEntityRemoteLoadTask.h + wonko/tasks/BaseWonkoEntityLocalLoadTask.cpp + wonko/tasks/BaseWonkoEntityLocalLoadTask.h + wonko/format/WonkoFormatV1.cpp + wonko/format/WonkoFormatV1.h + wonko/format/WonkoFormat.cpp + wonko/format/WonkoFormat.h + wonko/BaseWonkoEntity.cpp + wonko/BaseWonkoEntity.h + wonko/WonkoVersionList.cpp + wonko/WonkoVersionList.h + wonko/WonkoVersion.cpp + wonko/WonkoVersion.h + wonko/WonkoIndex.cpp + wonko/WonkoIndex.h + wonko/WonkoUtil.cpp + wonko/WonkoUtil.h + wonko/WonkoReference.cpp + wonko/WonkoReference.h ) ################################ COMPILE ################################ diff --git a/logic/Env.cpp b/logic/Env.cpp index c9093e77b..d66ec184e 100644 --- a/logic/Env.cpp +++ b/logic/Env.cpp @@ -8,6 +8,7 @@ #include #include #include "tasks/Task.h" +#include "wonko/WonkoIndex.h" #include /* @@ -138,6 +139,15 @@ void Env::registerVersionList(QString name, std::shared_ptr< BaseVersionList > v m_versionLists[name] = vlist; } +std::shared_ptr Env::wonkoIndex() +{ + if (!m_wonkoIndex) + { + m_wonkoIndex = std::make_shared(); + } + return m_wonkoIndex; +} + void Env::initHttpMetaCache() { @@ -154,6 +164,7 @@ void Env::initHttpMetaCache() m_metacache->addBase("root", QDir::currentPath()); m_metacache->addBase("translations", QDir("translations").absolutePath()); m_metacache->addBase("icons", QDir("cache/icons").absolutePath()); + m_metacache->addBase("wonko", QDir("cache/wonko").absolutePath()); m_metacache->Load(); } diff --git a/logic/Env.h b/logic/Env.h index 806fa106a..2b29acaa5 100644 --- a/logic/Env.h +++ b/logic/Env.h @@ -11,6 +11,7 @@ class QNetworkAccessManager; class HttpMetaCache; class BaseVersionList; class BaseVersion; +class WonkoIndex; #if defined(ENV) #undef ENV @@ -49,9 +50,17 @@ public: std::shared_ptr getVersion(QString component, QString version); void registerVersionList(QString name, std::shared_ptr vlist); + + std::shared_ptr wonkoIndex(); + + QString wonkoRootUrl() const { return m_wonkoRootUrl; } + void setWonkoRootUrl(const QString &url) { m_wonkoRootUrl = url; } + protected: std::shared_ptr m_qnam; std::shared_ptr m_metacache; std::shared_ptr m_icons; QMap> m_versionLists; + std::shared_ptr m_wonkoIndex; + QString m_wonkoRootUrl; }; diff --git a/logic/Json.h b/logic/Json.h index cb266c6ea..2cb60f0e5 100644 --- a/logic/Json.h +++ b/logic/Json.h @@ -113,9 +113,9 @@ template<> MULTIMC_LOGIC_EXPORT QUrl requireIsType(const QJsonValue &value // the following functions are higher level functions, that make use of the above functions for // type conversion template -T ensureIsType(const QJsonValue &value, const T default_, const QString &what = "Value") +T ensureIsType(const QJsonValue &value, const T default_ = T(), const QString &what = "Value") { - if (value.isUndefined()) + if (value.isUndefined() || value.isNull()) { return default_; } @@ -142,7 +142,7 @@ T requireIsType(const QJsonObject &parent, const QString &key, const QString &wh } template -T ensureIsType(const QJsonObject &parent, const QString &key, const T default_, const QString &what = "__placeholder__") +T ensureIsType(const QJsonObject &parent, const QString &key, const T default_ = T(), const QString &what = "__placeholder__") { const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); if (!parent.contains(key)) @@ -153,10 +153,10 @@ T ensureIsType(const QJsonObject &parent, const QString &key, const T default_, } template -QList requireIsArrayOf(const QJsonDocument &doc) +QVector requireIsArrayOf(const QJsonDocument &doc) { const QJsonArray array = requireArray(doc); - QList out; + QVector out; for (const QJsonValue val : array) { out.append(requireIsType(val, "Document")); @@ -165,19 +165,19 @@ QList requireIsArrayOf(const QJsonDocument &doc) } template -QList ensureIsArrayOf(const QJsonValue &value, const QString &what = "Value") +QVector ensureIsArrayOf(const QJsonValue &value, const QString &what = "Value") { - const QJsonArray array = requireIsType(value, what); - QList out; + const QJsonArray array = ensureIsType(value, QJsonArray(), what); + QVector out; for (const QJsonValue val : array) { - out.append(ensureIsType(val, what)); + out.append(requireIsType(val, what)); } return out; } template -QList ensureIsArrayOf(const QJsonValue &value, const QList default_, const QString &what = "Value") +QVector ensureIsArrayOf(const QJsonValue &value, const QVector default_, const QString &what = "Value") { if (value.isUndefined()) { @@ -188,19 +188,19 @@ QList ensureIsArrayOf(const QJsonValue &value, const QList default_, const /// @throw JsonException template -QList requireIsArrayOf(const QJsonObject &parent, const QString &key, const QString &what = "__placeholder__") +QVector requireIsArrayOf(const QJsonObject &parent, const QString &key, const QString &what = "__placeholder__") { const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); if (!parent.contains(key)) { throw JsonException(localWhat + "s parent does not contain " + localWhat); } - return requireIsArrayOf(parent.value(key), localWhat); + return ensureIsArrayOf(parent.value(key), localWhat); } template -QList ensureIsArrayOf(const QJsonObject &parent, const QString &key, - const QList &default_, const QString &what = "__placeholder__") +QVector ensureIsArrayOf(const QJsonObject &parent, const QString &key, + const QVector &default_ = QVector(), const QString &what = "__placeholder__") { const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); if (!parent.contains(key)) @@ -216,7 +216,7 @@ QList ensureIsArrayOf(const QJsonObject &parent, const QString &key, { \ return requireIsType(value, what); \ } \ - inline TYPE ensure##NAME(const QJsonValue &value, const TYPE default_, const QString &what = "Value") \ + inline TYPE ensure##NAME(const QJsonValue &value, const TYPE default_ = TYPE(), const QString &what = "Value") \ { \ return ensureIsType(value, default_, what); \ } \ @@ -224,7 +224,7 @@ QList ensureIsArrayOf(const QJsonObject &parent, const QString &key, { \ return requireIsType(parent, key, what); \ } \ - inline TYPE ensure##NAME(const QJsonObject &parent, const QString &key, const TYPE default_, const QString &what = "__placeholder") \ + inline TYPE ensure##NAME(const QJsonObject &parent, const QString &key, const TYPE default_ = TYPE(), const QString &what = "__placeholder") \ { \ return ensureIsType(parent, key, default_, what); \ } diff --git a/logic/java/JavaInstallList.cpp b/logic/java/JavaInstallList.cpp index fbd8ee9b9..c07292272 100644 --- a/logic/java/JavaInstallList.cpp +++ b/logic/java/JavaInstallList.cpp @@ -77,7 +77,7 @@ QVariant JavaInstallList::data(const QModelIndex &index, int role) const } } -BaseVersionList::RoleList JavaInstallList::providesRoles() +BaseVersionList::RoleList JavaInstallList::providesRoles() const { return {VersionPointerRole, VersionIdRole, VersionRole, RecommendedRole, PathRole, ArchitectureRole}; } diff --git a/logic/java/JavaInstallList.h b/logic/java/JavaInstallList.h index f2ec20f76..cf0e57847 100644 --- a/logic/java/JavaInstallList.h +++ b/logic/java/JavaInstallList.h @@ -41,7 +41,7 @@ public: virtual void sortVersions() override; virtual QVariant data(const QModelIndex &index, int role) const override; - virtual RoleList providesRoles() override; + virtual RoleList providesRoles() const override; public slots: virtual void updateListData(QList versions) override; diff --git a/logic/minecraft/MinecraftVersionList.cpp b/logic/minecraft/MinecraftVersionList.cpp index eab55c9ac..a5cc3a39e 100644 --- a/logic/minecraft/MinecraftVersionList.cpp +++ b/logic/minecraft/MinecraftVersionList.cpp @@ -307,7 +307,7 @@ QVariant MinecraftVersionList::data(const QModelIndex& index, int role) const } } -BaseVersionList::RoleList MinecraftVersionList::providesRoles() +BaseVersionList::RoleList MinecraftVersionList::providesRoles() const { return {VersionPointerRole, VersionRole, VersionIdRole, RecommendedRole, LatestRole, TypeRole}; } diff --git a/logic/minecraft/MinecraftVersionList.h b/logic/minecraft/MinecraftVersionList.h index 8643f0f09..0fca02a75 100644 --- a/logic/minecraft/MinecraftVersionList.h +++ b/logic/minecraft/MinecraftVersionList.h @@ -52,7 +52,7 @@ public: virtual int count() const override; virtual void sortVersions() override; virtual QVariant data(const QModelIndex & index, int role) const override; - virtual RoleList providesRoles() override; + virtual RoleList providesRoles() const override; virtual BaseVersionPtr getLatestStable() const override; virtual BaseVersionPtr getRecommended() const override; diff --git a/logic/minecraft/forge/ForgeVersionList.cpp b/logic/minecraft/forge/ForgeVersionList.cpp index 907672f23..de185e5fa 100644 --- a/logic/minecraft/forge/ForgeVersionList.cpp +++ b/logic/minecraft/forge/ForgeVersionList.cpp @@ -89,7 +89,7 @@ QVariant ForgeVersionList::data(const QModelIndex &index, int role) const } } -QList ForgeVersionList::providesRoles() +BaseVersionList::RoleList ForgeVersionList::providesRoles() const { return {VersionPointerRole, VersionRole, VersionIdRole, ParentGameVersionRole, RecommendedRole, BranchRole}; } diff --git a/logic/minecraft/forge/ForgeVersionList.h b/logic/minecraft/forge/ForgeVersionList.h index 308503e3c..62c08b2ae 100644 --- a/logic/minecraft/forge/ForgeVersionList.h +++ b/logic/minecraft/forge/ForgeVersionList.h @@ -47,7 +47,7 @@ public: ForgeVersionPtr findVersionByVersionNr(QString version); virtual QVariant data(const QModelIndex &index, int role) const override; - virtual QList providesRoles() override; + virtual RoleList providesRoles() const override; virtual int columnCount(const QModelIndex &parent) const override; diff --git a/logic/wonko/BaseWonkoEntity.cpp b/logic/wonko/BaseWonkoEntity.cpp new file mode 100644 index 000000000..f5c59363d --- /dev/null +++ b/logic/wonko/BaseWonkoEntity.cpp @@ -0,0 +1,39 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "BaseWonkoEntity.h" + +#include "Json.h" +#include "WonkoUtil.h" + +BaseWonkoEntity::~BaseWonkoEntity() +{ +} + +void BaseWonkoEntity::store() const +{ + Json::write(serialized(), Wonko::localWonkoDir().absoluteFilePath(localFilename())); +} + +void BaseWonkoEntity::notifyLocalLoadComplete() +{ + m_localLoaded = true; + store(); +} +void BaseWonkoEntity::notifyRemoteLoadComplete() +{ + m_remoteLoaded = true; + store(); +} diff --git a/logic/wonko/BaseWonkoEntity.h b/logic/wonko/BaseWonkoEntity.h new file mode 100644 index 000000000..191b41845 --- /dev/null +++ b/logic/wonko/BaseWonkoEntity.h @@ -0,0 +1,51 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "multimc_logic_export.h" + +class Task; + +class MULTIMC_LOGIC_EXPORT BaseWonkoEntity +{ +public: + virtual ~BaseWonkoEntity(); + + using Ptr = std::shared_ptr; + + virtual std::unique_ptr remoteUpdateTask() = 0; + virtual std::unique_ptr localUpdateTask() = 0; + virtual void merge(const std::shared_ptr &other) = 0; + + void store() const; + virtual QString localFilename() const = 0; + virtual QJsonObject serialized() const = 0; + + bool isComplete() const { return m_localLoaded || m_remoteLoaded; } + + bool isLocalLoaded() const { return m_localLoaded; } + bool isRemoteLoaded() const { return m_remoteLoaded; } + + void notifyLocalLoadComplete(); + void notifyRemoteLoadComplete(); + +private: + bool m_localLoaded = false; + bool m_remoteLoaded = false; +}; diff --git a/logic/wonko/WonkoIndex.cpp b/logic/wonko/WonkoIndex.cpp new file mode 100644 index 000000000..8306af84b --- /dev/null +++ b/logic/wonko/WonkoIndex.cpp @@ -0,0 +1,147 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WonkoIndex.h" + +#include "WonkoVersionList.h" +#include "tasks/BaseWonkoEntityLocalLoadTask.h" +#include "tasks/BaseWonkoEntityRemoteLoadTask.h" +#include "format/WonkoFormat.h" + +WonkoIndex::WonkoIndex(QObject *parent) + : QAbstractListModel(parent) +{ +} +WonkoIndex::WonkoIndex(const QVector &lists, QObject *parent) + : QAbstractListModel(parent), m_lists(lists) +{ + for (int i = 0; i < m_lists.size(); ++i) + { + m_uids.insert(m_lists.at(i)->uid(), m_lists.at(i)); + connectVersionList(i, m_lists.at(i)); + } +} + +QVariant WonkoIndex::data(const QModelIndex &index, int role) const +{ + if (index.parent().isValid() || index.row() < 0 || index.row() >= m_lists.size()) + { + return QVariant(); + } + + WonkoVersionListPtr list = m_lists.at(index.row()); + switch (role) + { + case Qt::DisplayRole: + switch (index.column()) + { + case 0: return list->humanReadable(); + default: break; + } + case UidRole: return list->uid(); + case NameRole: return list->name(); + case ListPtrRole: return QVariant::fromValue(list); + } + return QVariant(); +} +int WonkoIndex::rowCount(const QModelIndex &parent) const +{ + return m_lists.size(); +} +int WonkoIndex::columnCount(const QModelIndex &parent) const +{ + return 1; +} +QVariant WonkoIndex::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal && role == Qt::DisplayRole && section == 0) + { + return tr("Name"); + } + else + { + return QVariant(); + } +} + +std::unique_ptr WonkoIndex::remoteUpdateTask() +{ + return std::unique_ptr(new WonkoIndexRemoteLoadTask(this, this)); +} +std::unique_ptr WonkoIndex::localUpdateTask() +{ + return std::unique_ptr(new WonkoIndexLocalLoadTask(this, this)); +} + +QJsonObject WonkoIndex::serialized() const +{ + return WonkoFormat::serializeIndex(this); +} + +bool WonkoIndex::hasUid(const QString &uid) const +{ + return m_uids.contains(uid); +} +WonkoVersionListPtr WonkoIndex::getList(const QString &uid) const +{ + return m_uids.value(uid, nullptr); +} +WonkoVersionListPtr WonkoIndex::getListGuaranteed(const QString &uid) const +{ + return m_uids.value(uid, std::make_shared(uid)); +} + +void WonkoIndex::merge(const Ptr &other) +{ + const QVector lists = std::dynamic_pointer_cast(other)->m_lists; + // initial load, no need to merge + if (m_lists.isEmpty()) + { + beginResetModel(); + m_lists = lists; + for (int i = 0; i < lists.size(); ++i) + { + m_uids.insert(lists.at(i)->uid(), lists.at(i)); + connectVersionList(i, lists.at(i)); + } + endResetModel(); + } + else + { + for (const WonkoVersionListPtr &list : lists) + { + if (m_uids.contains(list->uid())) + { + m_uids[list->uid()]->merge(list); + } + else + { + beginInsertRows(QModelIndex(), m_lists.size(), m_lists.size()); + connectVersionList(m_lists.size(), list); + m_lists.append(list); + m_uids.insert(list->uid(), list); + endInsertRows(); + } + } + } +} + +void WonkoIndex::connectVersionList(const int row, const WonkoVersionListPtr &list) +{ + connect(list.get(), &WonkoVersionList::nameChanged, this, [this, row]() + { + emit dataChanged(index(row), index(row), QVector() << Qt::DisplayRole); + }); +} diff --git a/logic/wonko/WonkoIndex.h b/logic/wonko/WonkoIndex.h new file mode 100644 index 000000000..8b149c7da --- /dev/null +++ b/logic/wonko/WonkoIndex.h @@ -0,0 +1,68 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "BaseWonkoEntity.h" + +#include "multimc_logic_export.h" + +class Task; +using WonkoVersionListPtr = std::shared_ptr; + +class MULTIMC_LOGIC_EXPORT WonkoIndex : public QAbstractListModel, public BaseWonkoEntity +{ + Q_OBJECT +public: + explicit WonkoIndex(QObject *parent = nullptr); + explicit WonkoIndex(const QVector &lists, QObject *parent = nullptr); + + enum + { + UidRole = Qt::UserRole, + NameRole, + ListPtrRole + }; + + QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + std::unique_ptr remoteUpdateTask() override; + std::unique_ptr localUpdateTask() override; + + QString localFilename() const override { return "index.json"; } + QJsonObject serialized() const override; + + // queries + bool hasUid(const QString &uid) const; + WonkoVersionListPtr getList(const QString &uid) const; + WonkoVersionListPtr getListGuaranteed(const QString &uid) const; + + QVector lists() const { return m_lists; } + +public: // for usage by parsers only + void merge(const BaseWonkoEntity::Ptr &other); + +private: + QVector m_lists; + QHash m_uids; + + void connectVersionList(const int row, const WonkoVersionListPtr &list); +}; diff --git a/logic/wonko/WonkoReference.cpp b/logic/wonko/WonkoReference.cpp new file mode 100644 index 000000000..519d59aad --- /dev/null +++ b/logic/wonko/WonkoReference.cpp @@ -0,0 +1,44 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WonkoReference.h" + +WonkoReference::WonkoReference(const QString &uid) + : m_uid(uid) +{ +} + +QString WonkoReference::uid() const +{ + return m_uid; +} + +QString WonkoReference::version() const +{ + return m_version; +} +void WonkoReference::setVersion(const QString &version) +{ + m_version = version; +} + +bool WonkoReference::operator==(const WonkoReference &other) const +{ + return m_uid == other.m_uid && m_version == other.m_version; +} +bool WonkoReference::operator!=(const WonkoReference &other) const +{ + return m_uid != other.m_uid || m_version != other.m_version; +} diff --git a/logic/wonko/WonkoReference.h b/logic/wonko/WonkoReference.h new file mode 100644 index 000000000..73a85d76d --- /dev/null +++ b/logic/wonko/WonkoReference.h @@ -0,0 +1,41 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "multimc_logic_export.h" + +class MULTIMC_LOGIC_EXPORT WonkoReference +{ +public: + WonkoReference() {} + explicit WonkoReference(const QString &uid); + + QString uid() const; + + QString version() const; + void setVersion(const QString &version); + + bool operator==(const WonkoReference &other) const; + bool operator!=(const WonkoReference &other) const; + +private: + QString m_uid; + QString m_version; +}; +Q_DECLARE_METATYPE(WonkoReference) diff --git a/logic/wonko/WonkoUtil.cpp b/logic/wonko/WonkoUtil.cpp new file mode 100644 index 000000000..94726c6b5 --- /dev/null +++ b/logic/wonko/WonkoUtil.cpp @@ -0,0 +1,47 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WonkoUtil.h" + +#include +#include + +#include "Env.h" + +namespace Wonko +{ +QUrl rootUrl() +{ + return ENV.wonkoRootUrl(); +} +QUrl indexUrl() +{ + return rootUrl().resolved(QStringLiteral("index.json")); +} +QUrl versionListUrl(const QString &uid) +{ + return rootUrl().resolved(uid + ".json"); +} +QUrl versionUrl(const QString &uid, const QString &version) +{ + return rootUrl().resolved(uid + "/" + version + ".json"); +} + +QDir localWonkoDir() +{ + return QDir("wonko"); +} + +} diff --git a/logic/wonko/WonkoUtil.h b/logic/wonko/WonkoUtil.h new file mode 100644 index 000000000..b618ab718 --- /dev/null +++ b/logic/wonko/WonkoUtil.h @@ -0,0 +1,31 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "multimc_logic_export.h" + +class QUrl; +class QString; +class QDir; + +namespace Wonko +{ +MULTIMC_LOGIC_EXPORT QUrl rootUrl(); +MULTIMC_LOGIC_EXPORT QUrl indexUrl(); +MULTIMC_LOGIC_EXPORT QUrl versionListUrl(const QString &uid); +MULTIMC_LOGIC_EXPORT QUrl versionUrl(const QString &uid, const QString &version); +MULTIMC_LOGIC_EXPORT QDir localWonkoDir(); +} diff --git a/logic/wonko/WonkoVersion.cpp b/logic/wonko/WonkoVersion.cpp new file mode 100644 index 000000000..7b7da86ca --- /dev/null +++ b/logic/wonko/WonkoVersion.cpp @@ -0,0 +1,102 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WonkoVersion.h" + +#include + +#include "tasks/BaseWonkoEntityLocalLoadTask.h" +#include "tasks/BaseWonkoEntityRemoteLoadTask.h" +#include "format/WonkoFormat.h" + +WonkoVersion::WonkoVersion(const QString &uid, const QString &version) + : BaseVersion(), m_uid(uid), m_version(version) +{ +} + +QString WonkoVersion::descriptor() +{ + return m_version; +} +QString WonkoVersion::name() +{ + return m_version; +} +QString WonkoVersion::typeString() const +{ + return m_type; +} + +QDateTime WonkoVersion::time() const +{ + return QDateTime::fromMSecsSinceEpoch(m_time * 1000, Qt::UTC); +} + +std::unique_ptr WonkoVersion::remoteUpdateTask() +{ + return std::unique_ptr(new WonkoVersionRemoteLoadTask(this, this)); +} +std::unique_ptr WonkoVersion::localUpdateTask() +{ + return std::unique_ptr(new WonkoVersionLocalLoadTask(this, this)); +} + +void WonkoVersion::merge(const std::shared_ptr &other) +{ + WonkoVersionPtr version = std::dynamic_pointer_cast(other); + if (m_type != version->m_type) + { + setType(version->m_type); + } + if (m_time != version->m_time) + { + setTime(version->m_time); + } + if (m_requires != version->m_requires) + { + setRequires(version->m_requires); + } + + setData(version->m_data); +} + +QString WonkoVersion::localFilename() const +{ + return m_uid + '/' + m_version + ".json"; +} +QJsonObject WonkoVersion::serialized() const +{ + return WonkoFormat::serializeVersion(this); +} + +void WonkoVersion::setType(const QString &type) +{ + m_type = type; + emit typeChanged(); +} +void WonkoVersion::setTime(const qint64 time) +{ + m_time = time; + emit timeChanged(); +} +void WonkoVersion::setRequires(const QVector &requires) +{ + m_requires = requires; + emit requiresChanged(); +} +void WonkoVersion::setData(const VersionFilePtr &data) +{ + m_data = data; +} diff --git a/logic/wonko/WonkoVersion.h b/logic/wonko/WonkoVersion.h new file mode 100644 index 000000000..a1de4d9b8 --- /dev/null +++ b/logic/wonko/WonkoVersion.h @@ -0,0 +1,83 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "BaseVersion.h" +#include "BaseWonkoEntity.h" + +#include +#include +#include +#include + +#include "minecraft/VersionFile.h" +#include "WonkoReference.h" + +#include "multimc_logic_export.h" + +using WonkoVersionPtr = std::shared_ptr; + +class MULTIMC_LOGIC_EXPORT WonkoVersion : public QObject, public BaseVersion, public BaseWonkoEntity +{ + Q_OBJECT + Q_PROPERTY(QString uid READ uid CONSTANT) + Q_PROPERTY(QString version READ version CONSTANT) + Q_PROPERTY(QString type READ type NOTIFY typeChanged) + Q_PROPERTY(QDateTime time READ time NOTIFY timeChanged) + Q_PROPERTY(QVector requires READ requires NOTIFY requiresChanged) +public: + explicit WonkoVersion(const QString &uid, const QString &version); + + QString descriptor() override; + QString name() override; + QString typeString() const override; + + QString uid() const { return m_uid; } + QString version() const { return m_version; } + QString type() const { return m_type; } + QDateTime time() const; + qint64 rawTime() const { return m_time; } + QVector requires() const { return m_requires; } + VersionFilePtr data() const { return m_data; } + + std::unique_ptr remoteUpdateTask() override; + std::unique_ptr localUpdateTask() override; + void merge(const std::shared_ptr &other) override; + + QString localFilename() const override; + QJsonObject serialized() const override; + +public: // for usage by format parsers only + void setType(const QString &type); + void setTime(const qint64 time); + void setRequires(const QVector &requires); + void setData(const VersionFilePtr &data); + +signals: + void typeChanged(); + void timeChanged(); + void requiresChanged(); + +private: + QString m_uid; + QString m_version; + QString m_type; + qint64 m_time; + QVector m_requires; + VersionFilePtr m_data; +}; + +Q_DECLARE_METATYPE(WonkoVersionPtr) diff --git a/logic/wonko/WonkoVersionList.cpp b/logic/wonko/WonkoVersionList.cpp new file mode 100644 index 000000000..e9d793272 --- /dev/null +++ b/logic/wonko/WonkoVersionList.cpp @@ -0,0 +1,283 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WonkoVersionList.h" + +#include + +#include "WonkoVersion.h" +#include "tasks/BaseWonkoEntityRemoteLoadTask.h" +#include "tasks/BaseWonkoEntityLocalLoadTask.h" +#include "format/WonkoFormat.h" +#include "WonkoReference.h" + +class WVLLoadTask : public Task +{ + Q_OBJECT +public: + explicit WVLLoadTask(WonkoVersionList *list, QObject *parent = nullptr) + : Task(parent), m_list(list) + { + } + + bool canAbort() const override + { + return !m_currentTask || m_currentTask->canAbort(); + } + bool abort() override + { + return m_currentTask->abort(); + } + +private: + void executeTask() override + { + if (!m_list->isLocalLoaded()) + { + m_currentTask = m_list->localUpdateTask(); + connect(m_currentTask.get(), &Task::succeeded, this, &WVLLoadTask::next); + } + else + { + m_currentTask = m_list->remoteUpdateTask(); + connect(m_currentTask.get(), &Task::succeeded, this, &WVLLoadTask::emitSucceeded); + } + connect(m_currentTask.get(), &Task::status, this, &WVLLoadTask::setStatus); + connect(m_currentTask.get(), &Task::progress, this, &WVLLoadTask::setProgress); + connect(m_currentTask.get(), &Task::failed, this, &WVLLoadTask::emitFailed); + m_currentTask->start(); + } + + void next() + { + m_currentTask = m_list->remoteUpdateTask(); + connect(m_currentTask.get(), &Task::status, this, &WVLLoadTask::setStatus); + connect(m_currentTask.get(), &Task::progress, this, &WVLLoadTask::setProgress); + connect(m_currentTask.get(), &Task::succeeded, this, &WVLLoadTask::emitSucceeded); + m_currentTask->start(); + } + + WonkoVersionList *m_list; + std::unique_ptr m_currentTask; +}; + +WonkoVersionList::WonkoVersionList(const QString &uid, QObject *parent) + : BaseVersionList(parent), m_uid(uid) +{ + setObjectName("Wonko version list: " + uid); +} + +Task *WonkoVersionList::getLoadTask() +{ + return new WVLLoadTask(this); +} + +bool WonkoVersionList::isLoaded() +{ + return isLocalLoaded() && isRemoteLoaded(); +} + +const BaseVersionPtr WonkoVersionList::at(int i) const +{ + return m_versions.at(i); +} +int WonkoVersionList::count() const +{ + return m_versions.size(); +} + +void WonkoVersionList::sortVersions() +{ + beginResetModel(); + std::sort(m_versions.begin(), m_versions.end(), [](const WonkoVersionPtr &a, const WonkoVersionPtr &b) + { + return *a.get() < *b.get(); + }); + endResetModel(); +} + +QVariant WonkoVersionList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_versions.size() || index.parent().isValid()) + { + return QVariant(); + } + + WonkoVersionPtr version = m_versions.at(index.row()); + + switch (role) + { + case VersionPointerRole: return QVariant::fromValue(std::dynamic_pointer_cast(version)); + case VersionRole: + case VersionIdRole: + return version->version(); + case ParentGameVersionRole: + { + const auto end = version->requires().end(); + const auto it = std::find_if(version->requires().begin(), end, + [](const WonkoReference &ref) { return ref.uid() == "net.minecraft"; }); + if (it != end) + { + return (*it).version(); + } + return QVariant(); + } + case TypeRole: return version->type(); + + case UidRole: return version->uid(); + case TimeRole: return version->time(); + case RequiresRole: return QVariant::fromValue(version->requires()); + case SortRole: return version->rawTime(); + case WonkoVersionPtrRole: return QVariant::fromValue(version); + case RecommendedRole: return version == getRecommended(); + case LatestRole: return version == getLatestStable(); + default: return QVariant(); + } +} + +BaseVersionList::RoleList WonkoVersionList::providesRoles() const +{ + return {VersionPointerRole, VersionRole, VersionIdRole, ParentGameVersionRole, + TypeRole, UidRole, TimeRole, RequiresRole, SortRole, + RecommendedRole, LatestRole, WonkoVersionPtrRole}; +} + +QHash WonkoVersionList::roleNames() const +{ + QHash roles = BaseVersionList::roleNames(); + roles.insert(UidRole, "uid"); + roles.insert(TimeRole, "time"); + roles.insert(SortRole, "sort"); + roles.insert(RequiresRole, "requires"); + return roles; +} + +std::unique_ptr WonkoVersionList::remoteUpdateTask() +{ + return std::unique_ptr(new WonkoVersionListRemoteLoadTask(this, this)); +} +std::unique_ptr WonkoVersionList::localUpdateTask() +{ + return std::unique_ptr(new WonkoVersionListLocalLoadTask(this, this)); +} + +QString WonkoVersionList::localFilename() const +{ + return m_uid + ".json"; +} +QJsonObject WonkoVersionList::serialized() const +{ + return WonkoFormat::serializeVersionList(this); +} + +QString WonkoVersionList::humanReadable() const +{ + return m_name.isEmpty() ? m_uid : m_name; +} + +bool WonkoVersionList::hasVersion(const QString &version) const +{ + return m_lookup.contains(version); +} +WonkoVersionPtr WonkoVersionList::getVersion(const QString &version) const +{ + return m_lookup.value(version); +} + +void WonkoVersionList::setName(const QString &name) +{ + m_name = name; + emit nameChanged(name); +} +void WonkoVersionList::setVersions(const QVector &versions) +{ + beginResetModel(); + m_versions = versions; + std::sort(m_versions.begin(), m_versions.end(), [](const WonkoVersionPtr &a, const WonkoVersionPtr &b) + { + return a->rawTime() > b->rawTime(); + }); + for (int i = 0; i < m_versions.size(); ++i) + { + m_lookup.insert(m_versions.at(i)->version(), m_versions.at(i)); + setupAddedVersion(i, m_versions.at(i)); + } + + m_latest = m_versions.isEmpty() ? nullptr : m_versions.first(); + auto recommendedIt = std::find_if(m_versions.constBegin(), m_versions.constEnd(), [](const WonkoVersionPtr &ptr) { return ptr->type() == "release"; }); + m_recommended = recommendedIt == m_versions.constEnd() ? nullptr : *recommendedIt; + endResetModel(); +} + +void WonkoVersionList::merge(const BaseWonkoEntity::Ptr &other) +{ + const WonkoVersionListPtr list = std::dynamic_pointer_cast(other); + if (m_name != list->m_name) + { + setName(list->m_name); + } + + if (m_versions.isEmpty()) + { + setVersions(list->m_versions); + } + else + { + for (const WonkoVersionPtr &version : list->m_versions) + { + if (m_lookup.contains(version->version())) + { + m_lookup.value(version->version())->merge(version); + } + else + { + beginInsertRows(QModelIndex(), m_versions.size(), m_versions.size()); + setupAddedVersion(m_versions.size(), version); + m_versions.append(version); + m_lookup.insert(version->uid(), version); + endInsertRows(); + + if (!m_latest || version->rawTime() > m_latest->rawTime()) + { + m_latest = version; + emit dataChanged(index(0), index(m_versions.size() - 1), QVector() << LatestRole); + } + if (!m_recommended || (version->type() == "release" && version->rawTime() > m_recommended->rawTime())) + { + m_recommended = version; + emit dataChanged(index(0), index(m_versions.size() - 1), QVector() << RecommendedRole); + } + } + } + } +} + +void WonkoVersionList::setupAddedVersion(const int row, const WonkoVersionPtr &version) +{ + connect(version.get(), &WonkoVersion::requiresChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector() << RequiresRole); }); + connect(version.get(), &WonkoVersion::timeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector() << TimeRole << SortRole); }); + connect(version.get(), &WonkoVersion::typeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector() << TypeRole); }); +} + +BaseVersionPtr WonkoVersionList::getLatestStable() const +{ + return m_latest; +} +BaseVersionPtr WonkoVersionList::getRecommended() const +{ + return m_recommended; +} + +#include "WonkoVersionList.moc" diff --git a/logic/wonko/WonkoVersionList.h b/logic/wonko/WonkoVersionList.h new file mode 100644 index 000000000..8ea35be62 --- /dev/null +++ b/logic/wonko/WonkoVersionList.h @@ -0,0 +1,92 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "BaseVersionList.h" +#include "BaseWonkoEntity.h" +#include + +using WonkoVersionPtr = std::shared_ptr; +using WonkoVersionListPtr = std::shared_ptr; + +class MULTIMC_LOGIC_EXPORT WonkoVersionList : public BaseVersionList, public BaseWonkoEntity +{ + Q_OBJECT + Q_PROPERTY(QString uid READ uid CONSTANT) + Q_PROPERTY(QString name READ name NOTIFY nameChanged) +public: + explicit WonkoVersionList(const QString &uid, QObject *parent = nullptr); + + enum Roles + { + UidRole = Qt::UserRole + 100, + TimeRole, + RequiresRole, + WonkoVersionPtrRole + }; + + Task *getLoadTask() override; + bool isLoaded() override; + const BaseVersionPtr at(int i) const override; + int count() const override; + void sortVersions() override; + + BaseVersionPtr getLatestStable() const override; + BaseVersionPtr getRecommended() const override; + + QVariant data(const QModelIndex &index, int role) const override; + RoleList providesRoles() const override; + QHash roleNames() const override; + + std::unique_ptr remoteUpdateTask() override; + std::unique_ptr localUpdateTask() override; + + QString localFilename() const override; + QJsonObject serialized() const override; + + QString uid() const { return m_uid; } + QString name() const { return m_name; } + QString humanReadable() const; + + bool hasVersion(const QString &version) const; + WonkoVersionPtr getVersion(const QString &version) const; + + QVector versions() const { return m_versions; } + +public: // for usage only by parsers + void setName(const QString &name); + void setVersions(const QVector &versions); + void merge(const BaseWonkoEntity::Ptr &other); + +signals: + void nameChanged(const QString &name); + +protected slots: + void updateListData(QList versions) override {} + +private: + QVector m_versions; + QHash m_lookup; + QString m_uid; + QString m_name; + + WonkoVersionPtr m_recommended; + WonkoVersionPtr m_latest; + + void setupAddedVersion(const int row, const WonkoVersionPtr &version); +}; + +Q_DECLARE_METATYPE(WonkoVersionListPtr) diff --git a/logic/wonko/format/WonkoFormat.cpp b/logic/wonko/format/WonkoFormat.cpp new file mode 100644 index 000000000..11192cbe6 --- /dev/null +++ b/logic/wonko/format/WonkoFormat.cpp @@ -0,0 +1,80 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WonkoFormat.h" + +#include "WonkoFormatV1.h" + +#include "wonko/WonkoIndex.h" +#include "wonko/WonkoVersion.h" +#include "wonko/WonkoVersionList.h" + +static int formatVersion(const QJsonObject &obj) +{ + if (!obj.contains("formatVersion")) { + throw WonkoParseException(QObject::tr("Missing required field: 'formatVersion'")); + } + if (!obj.value("formatVersion").isDouble()) { + throw WonkoParseException(QObject::tr("Required field has invalid type: 'formatVersion'")); + } + return obj.value("formatVersion").toInt(); +} + +void WonkoFormat::parseIndex(const QJsonObject &obj, WonkoIndex *ptr) +{ + const int version = formatVersion(obj); + switch (version) { + case 1: + ptr->merge(WonkoFormatV1().parseIndexInternal(obj)); + break; + default: + throw WonkoParseException(QObject::tr("Unknown formatVersion: %1").arg(version)); + } +} +void WonkoFormat::parseVersion(const QJsonObject &obj, WonkoVersion *ptr) +{ + const int version = formatVersion(obj); + switch (version) { + case 1: + ptr->merge(WonkoFormatV1().parseVersionInternal(obj)); + break; + default: + throw WonkoParseException(QObject::tr("Unknown formatVersion: %1").arg(version)); + } +} +void WonkoFormat::parseVersionList(const QJsonObject &obj, WonkoVersionList *ptr) +{ + const int version = formatVersion(obj); + switch (version) { + case 10: + ptr->merge(WonkoFormatV1().parseVersionListInternal(obj)); + break; + default: + throw WonkoParseException(QObject::tr("Unknown formatVersion: %1").arg(version)); + } +} + +QJsonObject WonkoFormat::serializeIndex(const WonkoIndex *ptr) +{ + return WonkoFormatV1().serializeIndexInternal(ptr); +} +QJsonObject WonkoFormat::serializeVersion(const WonkoVersion *ptr) +{ + return WonkoFormatV1().serializeVersionInternal(ptr); +} +QJsonObject WonkoFormat::serializeVersionList(const WonkoVersionList *ptr) +{ + return WonkoFormatV1().serializeVersionListInternal(ptr); +} diff --git a/logic/wonko/format/WonkoFormat.h b/logic/wonko/format/WonkoFormat.h new file mode 100644 index 000000000..450d6cccd --- /dev/null +++ b/logic/wonko/format/WonkoFormat.h @@ -0,0 +1,54 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "Exception.h" +#include "wonko/BaseWonkoEntity.h" + +class WonkoIndex; +class WonkoVersion; +class WonkoVersionList; + +class WonkoParseException : public Exception +{ +public: + using Exception::Exception; +}; + +class WonkoFormat +{ +public: + virtual ~WonkoFormat() {} + + static void parseIndex(const QJsonObject &obj, WonkoIndex *ptr); + static void parseVersion(const QJsonObject &obj, WonkoVersion *ptr); + static void parseVersionList(const QJsonObject &obj, WonkoVersionList *ptr); + + static QJsonObject serializeIndex(const WonkoIndex *ptr); + static QJsonObject serializeVersion(const WonkoVersion *ptr); + static QJsonObject serializeVersionList(const WonkoVersionList *ptr); + +protected: + virtual BaseWonkoEntity::Ptr parseIndexInternal(const QJsonObject &obj) const = 0; + virtual BaseWonkoEntity::Ptr parseVersionInternal(const QJsonObject &obj) const = 0; + virtual BaseWonkoEntity::Ptr parseVersionListInternal(const QJsonObject &obj) const = 0; + virtual QJsonObject serializeIndexInternal(const WonkoIndex *ptr) const = 0; + virtual QJsonObject serializeVersionInternal(const WonkoVersion *ptr) const = 0; + virtual QJsonObject serializeVersionListInternal(const WonkoVersionList *ptr) const = 0; +}; diff --git a/logic/wonko/format/WonkoFormatV1.cpp b/logic/wonko/format/WonkoFormatV1.cpp new file mode 100644 index 000000000..363eebfb6 --- /dev/null +++ b/logic/wonko/format/WonkoFormatV1.cpp @@ -0,0 +1,156 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WonkoFormatV1.h" +#include + +#include "Json.h" + +#include "wonko/WonkoIndex.h" +#include "wonko/WonkoVersion.h" +#include "wonko/WonkoVersionList.h" +#include "Env.h" + +using namespace Json; + +static WonkoVersionPtr parseCommonVersion(const QString &uid, const QJsonObject &obj) +{ + const QVector requiresRaw = obj.contains("requires") ? requireIsArrayOf(obj, "requires") : QVector(); + QVector requires; + requires.reserve(requiresRaw.size()); + std::transform(requiresRaw.begin(), requiresRaw.end(), std::back_inserter(requires), [](const QJsonObject &rObj) + { + WonkoReference ref(requireString(rObj, "uid")); + ref.setVersion(ensureString(rObj, "version", QString())); + return ref; + }); + + WonkoVersionPtr version = std::make_shared(uid, requireString(obj, "version")); + if (obj.value("time").isString()) + { + version->setTime(QDateTime::fromString(requireString(obj, "time"), Qt::ISODate).toMSecsSinceEpoch() / 1000); + } + else + { + version->setTime(requireInteger(obj, "time")); + } + version->setType(ensureString(obj, "type", QString())); + version->setRequires(requires); + return version; +} +static void serializeCommonVersion(const WonkoVersion *version, QJsonObject &obj) +{ + QJsonArray requires; + for (const WonkoReference &ref : version->requires()) + { + if (ref.version().isEmpty()) + { + requires.append(QJsonObject({{"uid", ref.uid()}})); + } + else + { + requires.append(QJsonObject({ + {"uid", ref.uid()}, + {"version", ref.version()} + })); + } + } + + obj.insert("version", version->version()); + obj.insert("type", version->type()); + obj.insert("time", version->time().toString(Qt::ISODate)); + obj.insert("requires", requires); +} + +BaseWonkoEntity::Ptr WonkoFormatV1::parseIndexInternal(const QJsonObject &obj) const +{ + const QVector objects = requireIsArrayOf(obj, "index"); + QVector lists; + lists.reserve(objects.size()); + std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject &obj) + { + WonkoVersionListPtr list = std::make_shared(requireString(obj, "uid")); + list->setName(ensureString(obj, "name", QString())); + return list; + }); + return std::make_shared(lists); +} +BaseWonkoEntity::Ptr WonkoFormatV1::parseVersionInternal(const QJsonObject &obj) const +{ + WonkoVersionPtr version = parseCommonVersion(requireString(obj, "uid"), obj); + + version->setData(OneSixVersionFormat::versionFileFromJson(QJsonDocument(obj), + QString("%1/%2.json").arg(version->uid(), version->version()), + obj.contains("order"))); + return version; +} +BaseWonkoEntity::Ptr WonkoFormatV1::parseVersionListInternal(const QJsonObject &obj) const +{ + const QString uid = requireString(obj, "uid"); + + const QVector versionsRaw = requireIsArrayOf(obj, "versions"); + QVector versions; + versions.reserve(versionsRaw.size()); + std::transform(versionsRaw.begin(), versionsRaw.end(), std::back_inserter(versions), [this, uid](const QJsonObject &vObj) + { return parseCommonVersion(uid, vObj); }); + + WonkoVersionListPtr list = std::make_shared(uid); + list->setName(ensureString(obj, "name", QString())); + list->setVersions(versions); + return list; +} + +QJsonObject WonkoFormatV1::serializeIndexInternal(const WonkoIndex *ptr) const +{ + QJsonArray index; + for (const WonkoVersionListPtr &list : ptr->lists()) + { + index.append(QJsonObject({ + {"uid", list->uid()}, + {"name", list->name()} + })); + } + return QJsonObject({ + {"formatVersion", 1}, + {"index", index} + }); +} +QJsonObject WonkoFormatV1::serializeVersionInternal(const WonkoVersion *ptr) const +{ + QJsonObject obj = OneSixVersionFormat::versionFileToJson(ptr->data(), true).object(); + serializeCommonVersion(ptr, obj); + obj.insert("formatVersion", 1); + obj.insert("uid", ptr->uid()); + // TODO: the name should be looked up in the UI based on the uid + obj.insert("name", ENV.wonkoIndex()->getListGuaranteed(ptr->uid())->name()); + + return obj; +} +QJsonObject WonkoFormatV1::serializeVersionListInternal(const WonkoVersionList *ptr) const +{ + QJsonArray versions; + for (const WonkoVersionPtr &version : ptr->versions()) + { + QJsonObject obj; + serializeCommonVersion(version.get(), obj); + versions.append(obj); + } + return QJsonObject({ + {"formatVersion", 10}, + {"uid", ptr->uid()}, + {"name", ptr->name().isNull() ? QJsonValue() : ptr->name()}, + {"versions", versions} + }); +} diff --git a/logic/wonko/format/WonkoFormatV1.h b/logic/wonko/format/WonkoFormatV1.h new file mode 100644 index 000000000..927598042 --- /dev/null +++ b/logic/wonko/format/WonkoFormatV1.h @@ -0,0 +1,30 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "WonkoFormat.h" + +class WonkoFormatV1 : public WonkoFormat +{ +public: + BaseWonkoEntity::Ptr parseIndexInternal(const QJsonObject &obj) const override; + BaseWonkoEntity::Ptr parseVersionInternal(const QJsonObject &obj) const override; + BaseWonkoEntity::Ptr parseVersionListInternal(const QJsonObject &obj) const override; + + QJsonObject serializeIndexInternal(const WonkoIndex *ptr) const override; + QJsonObject serializeVersionInternal(const WonkoVersion *ptr) const override; + QJsonObject serializeVersionListInternal(const WonkoVersionList *ptr) const override; +}; diff --git a/logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.cpp b/logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.cpp new file mode 100644 index 000000000..b54c592fc --- /dev/null +++ b/logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.cpp @@ -0,0 +1,117 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "BaseWonkoEntityLocalLoadTask.h" + +#include + +#include "wonko/format/WonkoFormat.h" +#include "wonko/WonkoUtil.h" +#include "wonko/WonkoIndex.h" +#include "wonko/WonkoVersion.h" +#include "wonko/WonkoVersionList.h" +#include "Env.h" +#include "Json.h" + +BaseWonkoEntityLocalLoadTask::BaseWonkoEntityLocalLoadTask(BaseWonkoEntity *entity, QObject *parent) + : Task(parent), m_entity(entity) +{ +} + +void BaseWonkoEntityLocalLoadTask::executeTask() +{ + const QString fname = Wonko::localWonkoDir().absoluteFilePath(filename()); + if (!QFile::exists(fname)) + { + emitFailed(tr("File doesn't exist")); + return; + } + + setStatus(tr("Reading %1...").arg(name())); + setProgress(0, 0); + + try + { + parse(Json::requireObject(Json::requireDocument(fname, name()), name())); + m_entity->notifyLocalLoadComplete(); + emitSucceeded(); + } + catch (Exception &e) + { + emitFailed(tr("Unable to parse file %1: %2").arg(fname, e.cause())); + } +} + +// WONKO INDEX // +WonkoIndexLocalLoadTask::WonkoIndexLocalLoadTask(WonkoIndex *index, QObject *parent) + : BaseWonkoEntityLocalLoadTask(index, parent) +{ +} +QString WonkoIndexLocalLoadTask::filename() const +{ + return "index.json"; +} +QString WonkoIndexLocalLoadTask::name() const +{ + return tr("Wonko Index"); +} +void WonkoIndexLocalLoadTask::parse(const QJsonObject &obj) const +{ + WonkoFormat::parseIndex(obj, dynamic_cast(entity())); +} + +// WONKO VERSION LIST // +WonkoVersionListLocalLoadTask::WonkoVersionListLocalLoadTask(WonkoVersionList *list, QObject *parent) + : BaseWonkoEntityLocalLoadTask(list, parent) +{ +} +QString WonkoVersionListLocalLoadTask::filename() const +{ + return list()->uid() + ".json"; +} +QString WonkoVersionListLocalLoadTask::name() const +{ + return tr("Wonko Version List for %1").arg(list()->humanReadable()); +} +void WonkoVersionListLocalLoadTask::parse(const QJsonObject &obj) const +{ + WonkoFormat::parseVersionList(obj, list()); +} +WonkoVersionList *WonkoVersionListLocalLoadTask::list() const +{ + return dynamic_cast(entity()); +} + +// WONKO VERSION // +WonkoVersionLocalLoadTask::WonkoVersionLocalLoadTask(WonkoVersion *version, QObject *parent) + : BaseWonkoEntityLocalLoadTask(version, parent) +{ +} +QString WonkoVersionLocalLoadTask::filename() const +{ + return version()->uid() + "/" + version()->version() + ".json"; +} +QString WonkoVersionLocalLoadTask::name() const +{ + return tr("Wonko Version for %1").arg(version()->name()); +} +void WonkoVersionLocalLoadTask::parse(const QJsonObject &obj) const +{ + WonkoFormat::parseVersion(obj, version()); +} +WonkoVersion *WonkoVersionLocalLoadTask::version() const +{ + return dynamic_cast(entity()); +} diff --git a/logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.h b/logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.h new file mode 100644 index 000000000..2affa17fa --- /dev/null +++ b/logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.h @@ -0,0 +1,81 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tasks/Task.h" +#include + +class BaseWonkoEntity; +class WonkoIndex; +class WonkoVersionList; +class WonkoVersion; + +class BaseWonkoEntityLocalLoadTask : public Task +{ + Q_OBJECT +public: + explicit BaseWonkoEntityLocalLoadTask(BaseWonkoEntity *entity, QObject *parent = nullptr); + +protected: + virtual QString filename() const = 0; + virtual QString name() const = 0; + virtual void parse(const QJsonObject &obj) const = 0; + + BaseWonkoEntity *entity() const { return m_entity; } + +private: + void executeTask() override; + + BaseWonkoEntity *m_entity; +}; + +class WonkoIndexLocalLoadTask : public BaseWonkoEntityLocalLoadTask +{ + Q_OBJECT +public: + explicit WonkoIndexLocalLoadTask(WonkoIndex *index, QObject *parent = nullptr); + +private: + QString filename() const override; + QString name() const override; + void parse(const QJsonObject &obj) const override; +}; +class WonkoVersionListLocalLoadTask : public BaseWonkoEntityLocalLoadTask +{ + Q_OBJECT +public: + explicit WonkoVersionListLocalLoadTask(WonkoVersionList *list, QObject *parent = nullptr); + +private: + QString filename() const override; + QString name() const override; + void parse(const QJsonObject &obj) const override; + + WonkoVersionList *list() const; +}; +class WonkoVersionLocalLoadTask : public BaseWonkoEntityLocalLoadTask +{ + Q_OBJECT +public: + explicit WonkoVersionLocalLoadTask(WonkoVersion *version, QObject *parent = nullptr); + +private: + QString filename() const override; + QString name() const override; + void parse(const QJsonObject &obj) const override; + + WonkoVersion *version() const; +}; diff --git a/logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.cpp b/logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.cpp new file mode 100644 index 000000000..727ec89d5 --- /dev/null +++ b/logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.cpp @@ -0,0 +1,126 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "BaseWonkoEntityRemoteLoadTask.h" + +#include "net/CacheDownload.h" +#include "net/HttpMetaCache.h" +#include "net/NetJob.h" +#include "wonko/format/WonkoFormat.h" +#include "wonko/WonkoUtil.h" +#include "wonko/WonkoIndex.h" +#include "wonko/WonkoVersion.h" +#include "wonko/WonkoVersionList.h" +#include "Env.h" +#include "Json.h" + +BaseWonkoEntityRemoteLoadTask::BaseWonkoEntityRemoteLoadTask(BaseWonkoEntity *entity, QObject *parent) + : Task(parent), m_entity(entity) +{ +} + +void BaseWonkoEntityRemoteLoadTask::executeTask() +{ + NetJob *job = new NetJob(name()); + + auto entry = ENV.metacache()->resolveEntry("wonko", url().toString()); + entry->setStale(true); + m_dl = CacheDownload::make(url(), entry); + job->addNetAction(m_dl); + connect(job, &NetJob::failed, this, &BaseWonkoEntityRemoteLoadTask::emitFailed); + connect(job, &NetJob::succeeded, this, &BaseWonkoEntityRemoteLoadTask::networkFinished); + connect(job, &NetJob::status, this, &BaseWonkoEntityRemoteLoadTask::setStatus); + connect(job, &NetJob::progress, this, &BaseWonkoEntityRemoteLoadTask::setProgress); + job->start(); +} + +void BaseWonkoEntityRemoteLoadTask::networkFinished() +{ + setStatus(tr("Parsing...")); + setProgress(0, 0); + + try + { + parse(Json::requireObject(Json::requireDocument(m_dl->getTargetFilepath(), name()), name())); + m_entity->notifyRemoteLoadComplete(); + emitSucceeded(); + } + catch (Exception &e) + { + emitFailed(tr("Unable to parse response: %1").arg(e.cause())); + } +} + +// WONKO INDEX // +WonkoIndexRemoteLoadTask::WonkoIndexRemoteLoadTask(WonkoIndex *index, QObject *parent) + : BaseWonkoEntityRemoteLoadTask(index, parent) +{ +} +QUrl WonkoIndexRemoteLoadTask::url() const +{ + return Wonko::indexUrl(); +} +QString WonkoIndexRemoteLoadTask::name() const +{ + return tr("Wonko Index"); +} +void WonkoIndexRemoteLoadTask::parse(const QJsonObject &obj) const +{ + WonkoFormat::parseIndex(obj, dynamic_cast(entity())); +} + +// WONKO VERSION LIST // +WonkoVersionListRemoteLoadTask::WonkoVersionListRemoteLoadTask(WonkoVersionList *list, QObject *parent) + : BaseWonkoEntityRemoteLoadTask(list, parent) +{ +} +QUrl WonkoVersionListRemoteLoadTask::url() const +{ + return Wonko::versionListUrl(list()->uid()); +} +QString WonkoVersionListRemoteLoadTask::name() const +{ + return tr("Wonko Version List for %1").arg(list()->humanReadable()); +} +void WonkoVersionListRemoteLoadTask::parse(const QJsonObject &obj) const +{ + WonkoFormat::parseVersionList(obj, list()); +} +WonkoVersionList *WonkoVersionListRemoteLoadTask::list() const +{ + return dynamic_cast(entity()); +} + +// WONKO VERSION // +WonkoVersionRemoteLoadTask::WonkoVersionRemoteLoadTask(WonkoVersion *version, QObject *parent) + : BaseWonkoEntityRemoteLoadTask(version, parent) +{ +} +QUrl WonkoVersionRemoteLoadTask::url() const +{ + return Wonko::versionUrl(version()->uid(), version()->version()); +} +QString WonkoVersionRemoteLoadTask::name() const +{ + return tr("Wonko Version for %1").arg(version()->name()); +} +void WonkoVersionRemoteLoadTask::parse(const QJsonObject &obj) const +{ + WonkoFormat::parseVersion(obj, version()); +} +WonkoVersion *WonkoVersionRemoteLoadTask::version() const +{ + return dynamic_cast(entity()); +} diff --git a/logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.h b/logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.h new file mode 100644 index 000000000..91ed6af09 --- /dev/null +++ b/logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.h @@ -0,0 +1,85 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tasks/Task.h" +#include + +class BaseWonkoEntity; +class WonkoIndex; +class WonkoVersionList; +class WonkoVersion; + +class BaseWonkoEntityRemoteLoadTask : public Task +{ + Q_OBJECT +public: + explicit BaseWonkoEntityRemoteLoadTask(BaseWonkoEntity *entity, QObject *parent = nullptr); + +protected: + virtual QUrl url() const = 0; + virtual QString name() const = 0; + virtual void parse(const QJsonObject &obj) const = 0; + + BaseWonkoEntity *entity() const { return m_entity; } + +private slots: + void networkFinished(); + +private: + void executeTask() override; + + BaseWonkoEntity *m_entity; + std::shared_ptr m_dl; +}; + +class WonkoIndexRemoteLoadTask : public BaseWonkoEntityRemoteLoadTask +{ + Q_OBJECT +public: + explicit WonkoIndexRemoteLoadTask(WonkoIndex *index, QObject *parent = nullptr); + +private: + QUrl url() const override; + QString name() const override; + void parse(const QJsonObject &obj) const override; +}; +class WonkoVersionListRemoteLoadTask : public BaseWonkoEntityRemoteLoadTask +{ + Q_OBJECT +public: + explicit WonkoVersionListRemoteLoadTask(WonkoVersionList *list, QObject *parent = nullptr); + +private: + QUrl url() const override; + QString name() const override; + void parse(const QJsonObject &obj) const override; + + WonkoVersionList *list() const; +}; +class WonkoVersionRemoteLoadTask : public BaseWonkoEntityRemoteLoadTask +{ + Q_OBJECT +public: + explicit WonkoVersionRemoteLoadTask(WonkoVersion *version, QObject *parent = nullptr); + +private: + QUrl url() const override; + QString name() const override; + void parse(const QJsonObject &obj) const override; + + WonkoVersion *version() const; +}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 67a7a45e0..409462a27 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -37,6 +37,10 @@ add_unit_test(GZip tst_GZip.cpp) add_unit_test(JavaVersion tst_JavaVersion.cpp) add_unit_test(ParseUtils tst_ParseUtils.cpp) add_unit_test(MojangVersionFormat tst_MojangVersionFormat.cpp) +add_unit_test(BaseWonkoEntityLocalLoadTask tst_BaseWonkoEntityLocalLoadTask.cpp) +add_unit_test(BaseWonkoEntityRemoteLoadTask tst_BaseWonkoEntityRemoteLoadTask.cpp) +add_unit_test(WonkoVersionList tst_WonkoVersionList.cpp) +add_unit_test(WonkoIndex tst_WonkoIndex.cpp) # Tests END # diff --git a/tests/tst_BaseWonkoEntityLocalLoadTask.cpp b/tests/tst_BaseWonkoEntityLocalLoadTask.cpp new file mode 100644 index 000000000..74da222a5 --- /dev/null +++ b/tests/tst_BaseWonkoEntityLocalLoadTask.cpp @@ -0,0 +1,15 @@ +#include +#include "TestUtil.h" + +#include "wonko/tasks/BaseWonkoEntityLocalLoadTask.h" + +class BaseWonkoEntityLocalLoadTaskTest : public QObject +{ + Q_OBJECT +private +slots: +}; + +QTEST_GUILESS_MAIN(BaseWonkoEntityLocalLoadTaskTest) + +#include "tst_BaseWonkoEntityLocalLoadTask.moc" diff --git a/tests/tst_BaseWonkoEntityRemoteLoadTask.cpp b/tests/tst_BaseWonkoEntityRemoteLoadTask.cpp new file mode 100644 index 000000000..3a12e04e4 --- /dev/null +++ b/tests/tst_BaseWonkoEntityRemoteLoadTask.cpp @@ -0,0 +1,15 @@ +#include +#include "TestUtil.h" + +#include "wonko/tasks/BaseWonkoEntityRemoteLoadTask.h" + +class BaseWonkoEntityRemoteLoadTaskTest : public QObject +{ + Q_OBJECT +private +slots: +}; + +QTEST_GUILESS_MAIN(BaseWonkoEntityRemoteLoadTaskTest) + +#include "tst_BaseWonkoEntityRemoteLoadTask.moc" diff --git a/tests/tst_WonkoIndex.cpp b/tests/tst_WonkoIndex.cpp new file mode 100644 index 000000000..076c806bf --- /dev/null +++ b/tests/tst_WonkoIndex.cpp @@ -0,0 +1,50 @@ +#include +#include "TestUtil.h" + +#include "wonko/WonkoIndex.h" +#include "wonko/WonkoVersionList.h" +#include "Env.h" + +class WonkoIndexTest : public QObject +{ + Q_OBJECT +private +slots: + void test_isProvidedByEnv() + { + QVERIFY(ENV.wonkoIndex() != nullptr); + QCOMPARE(ENV.wonkoIndex(), ENV.wonkoIndex()); + } + + void test_providesTasks() + { + QVERIFY(ENV.wonkoIndex()->localUpdateTask() != nullptr); + QVERIFY(ENV.wonkoIndex()->remoteUpdateTask() != nullptr); + } + + void test_hasUid_and_getList() + { + WonkoIndex windex({std::make_shared("list1"), std::make_shared("list2"), std::make_shared("list3")}); + QVERIFY(windex.hasUid("list1")); + QVERIFY(!windex.hasUid("asdf")); + QVERIFY(windex.getList("list2") != nullptr); + QCOMPARE(windex.getList("list2")->uid(), QString("list2")); + QVERIFY(windex.getList("adsf") == nullptr); + } + + void test_merge() + { + WonkoIndex windex({std::make_shared("list1"), std::make_shared("list2"), std::make_shared("list3")}); + QCOMPARE(windex.lists().size(), 3); + windex.merge(std::shared_ptr(new WonkoIndex({std::make_shared("list1"), std::make_shared("list2"), std::make_shared("list3")}))); + QCOMPARE(windex.lists().size(), 3); + windex.merge(std::shared_ptr(new WonkoIndex({std::make_shared("list4"), std::make_shared("list2"), std::make_shared("list5")}))); + QCOMPARE(windex.lists().size(), 5); + windex.merge(std::shared_ptr(new WonkoIndex({std::make_shared("list6")}))); + QCOMPARE(windex.lists().size(), 6); + } +}; + +QTEST_GUILESS_MAIN(WonkoIndexTest) + +#include "tst_WonkoIndex.moc" diff --git a/tests/tst_WonkoVersionList.cpp b/tests/tst_WonkoVersionList.cpp new file mode 100644 index 000000000..7cb21df70 --- /dev/null +++ b/tests/tst_WonkoVersionList.cpp @@ -0,0 +1,15 @@ +#include +#include "TestUtil.h" + +#include "wonko/WonkoVersionList.h" + +class WonkoVersionListTest : public QObject +{ + Q_OBJECT +private +slots: +}; + +QTEST_GUILESS_MAIN(WonkoVersionListTest) + +#include "tst_WonkoVersionList.moc"