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 be1797cb..62bf53d7 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 edba18e3..64d07065 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 e3cadf74..5983fb42 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 70894592..22df7e09 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 00000000..4d376fdc --- /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 00000000..2b87b819 --- /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 17ab79cd..5d7c7968 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 9ddbceb1..28d4e639 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 00000000..21de2347 --- /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 00000000..fd77ee0d --- /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 00000000..2d14ceca --- /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 zcmV+-1Ks?IP)pH#7RU!R5*=wl23>mbri=x?>Cc4W|C~O z$tGFN%66rd?Mhp5wH}HFiU&mmy?CpaLiNzzN~tIwLTh$sm0P!y@) zp+(Uu+ZMNLmo&|v$xP-?W`3T!Z0h=skI(z?zKvsBzSVe zfPJfn$Q?XN*@|gA_dITK34P};>`BT*CwW~g{BUKn`^)wQFYSG*mP@0C*6^5Z(2xN$ zgLN`-NaByE+YwILVnZncQ4HOoH5Eb9@w}}khn2ukQV2Y(;SfKH8Q=PqhLNBOMd}K| zLaBY>_q!LT1mCRxaXm0+&h-t&My&9~V5`?W&*QTgvmu+#I!1i7@cFA0z~ZBGPL8e?Jl6=>Ep>#oX|6&dRW7=>VhtJvKXA zT;K9otSo%FcJ1bC4_I?Ifn%9wvsPi?joJL?Z=$hF&MA0~ZGJS#7ccK*|06iDy6RZ@ z!IAQOldR{HaVkV^m)KNs?aI}U*Z#bh?Bus2v|26mndS!sTZGqqgr41uv%~KQ<*Gr?31-j&;jad(-eXng9R*07*qoM6N<$g4VBu761SM 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 zcmV)?K!U%CP)pl07*naRCt{1y??N6TUOWi8FS9H_CEWZ zbMJfibw8j1n}`Or1shQ4RzU#~OanIPBS;6^(n=$ZznT^$M6nt)Rf>o}LDWRcf@mrs zV$2VpQ+<40yC2tw-w>yX2#N0*}^*zJAXy`*nYC7>BPszr5h=_9bb%xFv0O zq}`5TcTPMz`-yM*wm8lqyS!|>yLcxfAT8^>^H|eJtoKFS5Eiua&`AE z`+E-!Mr(amY$F2yq~cn`eLjpZHI|`9nRA zZ~eO8_Z8FO_(gZ0e$Q*Kudly|dC%641S%mVhK&pUaB;zKev5c^epiL>e@^l@eb4uN z&-8rUn@8&-w;m~g7vcK$-}=tal@xvtW?vqrBi!6)zIu-RbN4trxPqM74gxTucYouX z=UKnu=KA_`?!WhG9z6RDdbnbv2PiWt5XUoy^Gk+{OVY(9>Ebq=UmR}VdEz^Cmhbw# zf8t%wyioV;(fZI=`e>iPqvh)xzT&NK9z|}CK*=|69&C7XGDFINI^V!_4bv4eUlU4U z2qUQqaTY=>)EuxpB6SDlj1UH5fm#eC!jK-TL;5!4oMb}C^T&SA*T3^0)(BS1zj?>^ ze%IeVJ)iIYoxk^sr(AxY91d^P2Y1oK6{F3BplmRTX0j>(DMn%pgb>`@7{;@p`UfQO z7lB6#;6=P1c>o{9_3iI`=XsgZXJ0?_EOVa!TtZ$OwURJ4F|tjGs1x(O_j0`d3^&i- zCGT$-5pM0yxPAMQ^UDjuIPl!zh8iQf*<#~{N@Oa=oE23e#KdMCxpn(7(q@arhzuhZ zBhxhfoqzk!-u}De9RNK`FX$K7ye1+=~Iu--DP@k52ho78KV{S<~hpoKvu=l z2%9r(dybsnVtD)s(w!%e+jr2Ie(TKUue|d+-}MNXe37n49>7Ooz3r)|Zr`rwPhQ_W ze#*Ex`yWM&4N9n$ayW39j?^H?W@Hl-%Qw{d8qGIQ55yQq1cD}PHcAZ~9z4g!}Jk3Y`#{DSSp1s9J!PTZU^UtN7}gdYh(2|yI%?oa$nIPTG#D@Ijr zZ$>W0fmq>cI-(_0FlvFhR;Fo&To{It@!}FGnLLbtZY$-BfWPt4zUPnDPrn{1fERGR z?K3}fm@Y2jIh(Jkv3}3t{$0W_5RM17ArMU2&j;q~`y6W_jw5js*vL%PnJk$|A(&9g zf&EQpzlTiaP%?*7nG93{goIi^REP+Hh~~mR?>P(u+ue?Ec#_M<9%nPp?4~Qurw43P zV%Qt`O37FEVV>A*HY7QxWH{bjb3R5ij;IJ(p-wYpzo#6JgdwrJIOqJa+Y~DlWb@xX z^;G=IAN;}k(Z2tW)(5Uf3g87?<<{fhDf##x-usDvx{bB40mAtOQmdo&3U$uN;Xs)V zJSdqdR-U*_$nl1lkC@GfB#4FvBbUrE&sY#5#zrf}46_L{VI&|TAdw0L6$aZ7row~w zTyy<&B!s|a5H4>;o_YFd${~dZhT1wqRm6pR1_q$(j#q%t8|NmP#aud#BZ&V^bHD1sZ9l3v|Izw)u15;s1z1mg#vA{X%~xN+d~-+2kz|=63PTWd zo-kFEh)Do7tQ4r3fFY)o`G{!-0}(JYYN;4dO;IySwdM1~8oFy>7>Hp4Si!2GfQdm> zOsju}1Y&?OMgLq3(~261xFU!lfCLUXQ);1J6{cw-ri2113aXXE)qVcO|MgE8?!1cl z_^Xj&^HI6)kJblYj}*Z3xSsk&zvx2J#UBpy{;N`*Z;xa~3ZrGlVk7}mP&Ld1L%pfV`ivF zVws4wxQiYlwR)-!z)aCn5mizUQV^_qzTJXb?k?c9{WgPms>)sBJtSzqI|3IXq!5Tv z2((ZEBOpW(&No|xim5{Tku^X82}nw4HEJ%D;zv$HMB+d;IAmq7aQWoNar-r&$nLSn z|I+5{{7=5+2mb3H1NblA@s2y18FWI*M6lvF zKSWA37i8SM*Q$s@3J`;kXBT>cb9zjKYDQI-_l0}EFvH9c1T{F8!hR~u1nzv?$MeJ|yq3-5PatvpAr11E z7|&pP_L>~G-(Ih7uw1yhx@InwG>%jWJUASY?Us-Kxj(mv>F>h!|IL5=osWu~o{#k? z1b80S*=G1m((Vs$uY@y%5o1SXW0ho;AWCqAj^kA59()L{kAnaLF+d1L2vD@YCK95D z3Rc}Z8*Z%Me}%AW_y8>%08s)_8c0aT3_KhtRtaK61Q8*K;BS+LKM1)LhCxWf$dD32 z1g({M&eSSwlW>+sHdJhXg$9A``B&Ix`xPPq0Y;Ae|&HIE<(SrmBRq zbMC38%O{`wzI;5E|Lo0Q^F!bK-~Yg~&+8ZW3S9AnzvR*SP}Vno*>Al(6W(wp_N8a} zhRdLwM>tQgmC9h1U`oUVa+(!YH?Gp8bx{c7)>=aJm|lbsBOxY`(2YA11F}eSd$9de zM383fMXY59gb+@J8yYl7eCRQ^`eN3+0Y9F$a}@B`1DIm!M4}-EObxA>`EcZLbIp9% zqtgMM4$RjNINZIC3!g{0teirMgr=E(hd-vv(r~9fa3Gc>I%J_cqh=z4huoJDoTa zWgCob6h;)$f|f$96;-9!>ZwbRCBD}t#3n+FiBgqnhKYCxpvqjUhYt#y%}5Br-_r!Q zdF+c$=w3o-X@YKzLuiTuV@%OK1+yi5utn1tAPk$4qLq>}wG{u!6|0pP0-IsrcswvB zuMyCB=H}`?5ANON=H9c+*VpLbh6nF^53y!$oo#she8&^#J6?5p!JYFRu@;_t@6$-B zL^F057he*0m%pcOhxGdY?eqWUkMg4rBcG4fhqWGI03YUh`+xA(kF`zsM_2EAUzo11 z2sJYRQP_oAZja^2t70V{A0XvOBD0Z7l!DbvsnrEKMrJjNxuuTD{dTK`I#)_zvB)6? zR6rZ@uWPd3g|;p0*1G@nOh8eF!NES7RZk(1LO{&WS{eFho%}4v5kLYV4a}utxq2qS z*3@H+!Vn|JsSr{mqya*tno%%JA|W}3aJ|1_GYmX_`*AMMF0o=9Zw}0Jr4pGDbl75> z9qHC>hR2>jE-$Wi9R91ffB&EUx)<{Mez~qk3gE+7Z~C-1{$dH~`_E!{eXK>cK{y*C zV+by|#{+V>W}Nm69^Qihc2|x3^#z%6hOet-SreBDMZ8+tC?UQr~rbjiU0{HLavI{4iK&iAO$~W z&J_vG%V_)oYv6{7E3$0vl3eUAxY+KHYRrd>76VBX6RHf%63N2|y9+qKfU~n_({A@K z&u-uPso(wsfA-fupWpS%eLYeDAL4rRr~mRdirF6ts-Jfjja!>7w|851n~^9+IUYIQ z+%R9=<9PR3HdcugqRj-I398MKLKUb63Ty(7@Hw*wiy_Q5iQ2yaAd#VQ>r0J z2q}UH)s)hXX|MnY(%p7e1rP;d48&&fgER#Y1HoMctdx`V;Hm%?4}d_`Mm$nl@sPMD zfCHC9h^T5)bj}}$DH2+$qNp%PHAO7)w_*2jKK6p8;M+)F)u)gLC-uxx6`?!z)5}l8~DU|t3Q$DhbMyeAs zA1H?_rt5p`AKYbsb&uoziaJk3F;C-B5xkGym;%F)9JLPuF(EO5L{!Z;xEaBeKnoSj z$fY`PXzFAjaq!R?elD+k;Y_dGv4;pMj;g#iEVCBN%yzWz-&?|bI#aDDF^%Hie> zy1!4#M>bTp!3a7dC4*+r0#@l_7Z8Rp5XF!>6Rfa}upJU9MoO*BMUga8EKnK; z31$KNQVgvq34*lDgQ|BQGS4b(#>kK&V@gB`3+>p{E0s(HhJjKmDfnw^&df)zjfhRj zRD&>#jJq9ab4Cr3sZ^#?s1mX9l6>|AoIS?4yC6#Vex~_*6;k{-?@o*?3Wgu|MJz{XI_2%-lv%#+$H2aXHvOHuuB4U#`1*K z8E|TEXl}Jz&`1hGs2S1BHVNk&C(q=f%(*}sDHY~y)Kaiopq1U4f(e9C&=66PrUaDI zpK~Z+B?@r@{=upY07THb#_B0}&OeIB?PF|w0kg&g)WsclPO&u5Pg;!cO2iDRdUFuW zkG5kLP#T3D`OgnDUj!hEfNzMbL=Dx z$Z$yxx3S@pIQsKNU^b-$p~i#`kvvboVSlAR{O$kC|8Vs}?4g&+dZYkO)>BWt0Mt4(=bM=yh03ih=1uO(YObp{0!+4AH^G`t11x936 zZH-YBhD18QV7NHrnX79A`3>7!;jg{(2Y>L;zw@`f?QO%S9OX#>&VU0P4(;i)Gs0y$ ze)IQ!@5`2f_=pO8vhIx2o3Zd0>NIhD?k?ANpJBRsj+hUOQP~ZVT@_;tE+Y@{K&TOI-?^&tg-8KVBT#7N17qMo5h zExd>gr^pH=!>|>K8M#&>MyX1n5u&0~p3lw#xs+Bfp_n$1l1i{bBy%weF$rqMA!k%4 zG*@6?s{>mbNaq`7`0uFt9|3={-;FYGa|J}^k)Pb293jvFsxkz>M*MK*DWOv$& zXFc)&+WHM&^i{uI=i|4P>j$rjhiitK(dmHY1IY^8pqzzDvP{s-7z`^Ft#fmSo9iC} z5)%CFRFOF@r&4E|dL;=mf`hhFfr z$g5M0%`Lf=ra;VJ+h3Pr3kPCsbpnkN+$^_RUCWk(hX9Co4iX7iHMIB#g%F4#QuBmX zqvM^>U=r4>~St{KlY4D*zb==ra5CF4jK8lX+BbG=E42@9CJa& zk+eH!yts9^{V`9z@BGe_4-T=u@pruIUDq$rmV80hqbvaMCBN#+-(j`BQ%Zi#P-kwB zfl((Y6QN8*ok%((Wy11=)XW${v(_$ot!@qw%)BzLn6`!-jSPcZDQng@NVl|s7+dQu zyvDzpyY;z8Aw*(Iu83k{o@X#w zsw7QYJwwA;Osj*ZR;YR6aJb@dxTfYK!7|i|a=c;QUm)SdVZrgOY z)crN(`T_I(yVRR2n2+dmBp>$FX&$1;?KGr2(bBJZ)8~BtkNnSn??>-FuN(3OT#pn0 z@Y;|4q_>FKuN?#~#>lN9pwkWcaD&yEh!Uv;lqALwaWYNk*wWl+TWPx0TsYuiQcYJA zQD}MbmByRJbZuICa%Ac_N$d7c}gNU7#2 z&(PYHnHi;2rg?7o!I9~B!#p3TWulguJWb5UBY8eB9rhfqubK8&l;a+okJRIVvcG2D zUlB^-Vl%SaI4P;-Ub6v63~|_e!m-+?eBNjM%75}B|KRUE{gK^;AD#7z#{j zci;E)FRwYXx!7^J-IAs&v}E!;5v`I^V30&IVGMyG7+T6&wg+xCare6O^u4&p7Dk+% z?*Aop_G8pq-QCyL<;S0NLDE&xK+B;=LBuUC0>hA&oV_6@gdYznR1G1}vUZ3x1=AHq z7tpp$`?ahg#FALj-dyL4zy3@Y7s-jDcY0)L)u%Q6rZ^>=lSH zct3*{63L>i0)~U4b1lqucATQ!Q#GNM83;~Cj?wAMKpF-%n=LU7t!hz8YHYXX+&;e` z3|nsI%-Nhd5SUBlaQ7aS#OBVce%&_4?*e}O!`^f+qVB6?e;o`z;>-%B| zNEFmUs}ET7=FYocy^5+zDGn}*bt~Njr~!^43Ms8tbz%E859d^wEK+>p`DhO-+*A?_ zr!=cA3XWca+`JPbpbE7rBGto$<(?=-1gVu_7^$_OTB+?`#S=xRVjxN_+U}(*7O}2e z1qq397@-swc2QCkqGf7T0z!%-sTQ_XiP4Y{7|(Xpam%&B-D&3L*=LbqgABt*X79ZS z)+=5CM5_dK*4;KncEdo9k?oK;r0BpMPKME1$(B+4-y*t{%bp6jgy6AmPn&fmfIVbM z)gF$)7I!@~etm1kSyY$jqKK!>QtJ`GOjZ|wUPu(1iwvs}*zy5_;4=tRrFj#Mc*ht$ z=3ZWB9=9}*&a9;+*K~fnH-@QUs)#8y2wHRvd)hH#Xy+b05Spgdi|S#LdkC(4)XZ6u z7&YgG>p)Q~Iv7}+2c))wqKGa9Oo$PsG6n}86&Qjskdf+2CdSAn4p_oc{~i`!K9O_y;4gW3uUIR|l);PvC0Zrc${1mc0YR}^F!i)oRrNO9 zrXCyjAM+Rn!l9U?;-7>9w_He=3( zJZJKpndj_jxJDngjwfKt=gEne0R6E*cVqhBOD3RSf}KP@9b9PXy{b^EF&8H)6>ImU zw#KI|lri%Vs^o$eCp`6P!tUamc&&sr?@AGhL9Q?tWy*yq7v@qastYdBUsF3#N+6iF z7VE|ka6G4Gq|78Mj3|Ruwn31b(Zd0mkHnJMU_7=vBvU-`G5RvZ+q4A zxGi6>^@{cY{?KoG$DN=2e}8JT!H7Vr6`3cb&Tb`waVXi_WJQRQ@6zG%(I*4vGW<+AsrMybb9s$sUGETBV|)nTMZX zJ*TRMiIQ4~)j@=A>8-f~wcpp4Vd#prXE)4*QjHw)!c{Q5u}X@batx+uo=I^;YGH^G zBO-aG>~Amg}0id2r2mzWbiQ<_P?kKg%unQD3iU1rX!*uU>98zbxYgJ4t6k zHMT=Q$HYE~6Mf1=QNaRegRav?7UAhFvfwlg=Kz+}(}0;ZWaxi3L6lzL(}k?30O)() z7OSngBI2q_$0NHk67d*ZO}!~cCj?Y|zs4cmm3J1RoSOQ6e!T#%LM?7#MOtc43=N*q z@Q|kS7Q8%y2s92v2rvwhv+c+bz4qaFbLDP6C!c4uF3B$7<92X%WpoyxwBo0Jt<@m2 zG9r~le}P2rxlkK_K+TndLJAH{lDr^Eb!LD6StM*YJHH^EK|UH;Ae@a1kKf^Z3`~ay zgqt14srGv5u2-A_c>7ns{k6e{AK`ezbMN`dPsmsIg5)E0I&go!LC=KUP?!(b1TAca zKqi1yOj|B~@JhCXD}iE8jjguy+;YZS_DvFk^Xt2144oL%&gm3=7qK-j-z>gXCA12{ zS}XhM$gGtRf>V2OmzNgjN(-i|89A4wmS+g7JKwv0(_DOOLN*RtOug`^R)4RGLNLMr zQQ?@u8~r42Yu-X5ZPzHYkR^F~Qkqq+xsqc>hJ=_f4n`^MJhXWX?cBS7=i&gT(fJA8 z)ADd77lj0p{QiVSqN)aSbq%N4mD3mkDY=_aic*itG*1knUrRy)6wQhZrdYU2r%;DjL-D1u=#v^MD4ilA(HzN&e= zFQDQ58kjZmRZo2yrzu2PFH5XtP9loq4g@c=E>@{xgdt)P_VbbJ{hrp;GP2cbefEoN7rqvbKS2sKxFU;T&+{vd9N2XPWm;)Dp) zQ(z5S071n)fMH08L8)%>9bgR9Qkk;%`cqa+qGuCA9i%>qSoppB{OQ{`-d3YSh7ONUZxB#=w~T9OA#ZQl7O*|L!&CC!Wd!;pKJkDn=`N| zfQyUsM))brbDI8( zJqN%ru)X~l>GG0mg<}}F{J4+j?8%R#44Z#^zt$h0(-zsDzvrFb`Ypfz`QOPO+4YJE z0Sx=OLkwRyTBT|sYGx2+W5PBli9)oDXaV)#2_VSYyu*(}HDxY!b*Ig(kv5QxhO|>x zfjG6qUNYQk?|pR|@PZ5A|1f~ir}cq4GPitIY435uj5Bw-g$zq=LhK$hn6~ae4R=_q zy(tL{f}?zA7tbg3;O@FZC6*eHo$U zCp~&He1@Vf_eM?8s?5i0iWtYL47VPm?jLZt8!4OJrxh7Ktqhq^>$89Ko!@v!TK?!e zzw&O^p{wkzr5Z&VAPo{h4Y~7LSLfH*V+}x~G z1C&%*n{hOOR9hIWYb$M9{ywY)KrzVT`b%fG_2cMCW`T}yyXH{(^R_eut?C;(B%yok zt)<>lm?W+4-#Vv(Ug*;qgR83_LR^&4(tWtc_tvHhhUlreMq73Tz?uSWWFO2Gz>or) zA+c$NMq(#73R$urEo5JsorLHI%+;qL_e_Myddfjy82tD{ip}bu%s&wSgcL%%sl5`a zDf8CXz)mKrhUh8W)(feHdB!|Xs2SJSH{4ubb37iIjz^~bo|^M+7=~XSwz~(v`g4Be zKmU;*`>~gV5_rXU0B9xDLb6Is!bSsQRbrixdVo43TD+!T!a^`Hb>Zs*;qAPYnEcjx zSKg*DPWbG7$tqjxGM02-&jJ981+)_du(-%Q_U|r!8|&SbPU&UD(CdNeJp<|Qt4dfU zZ#^;LBs<|Dhb`TU*Yor1^$)Egt`N;T_DSmv7$T&m*wEToyUtw*K@fGTPps6^>~ACf zoSqUi5HMG=_WlD6D0F#TGA_xC0b}fpO7T=*2oPvo3|T-3f3)z-rRKWFU?Pofab z6^UVp&Pah+D$|3^gQ-x5EyJzH(anwnk#v5Goy0M2&i?pC#490f2Rq0EF@ z7{%B`7)(iJCh1HxFZwYva$bvF`XG*Oal2Ivtv&ZjR5@wS-SgiI(Pzb-tO@@z(brf# zfYXFIEW%K`)wea2=mNVa09|B$v5uY+Q>W@G1l46~u%FX<4jm70;*hn{Ut2N=oOmQT zH|lPez*fx|Lu}p+U=^u7#%>sgD$<6H#%5J(Q!xF?m-{5$1F2pL-LeErL6h}?rXX60 zT1e)?UPUpKfHx%(@o{Ei>V;D=EE$wing=C@btNNZE!47#RQD#5R*-=+bLE>%IUEk; z!vu37KK2B|t$C@Fcqyz`i~^`RL(R_cLl^?2R^69^EyX9Lw%l?PE;k9*CiAYAd7jJS za#vPVeX&jrIP5ZV$_Ug|?v}?F!zW8pp;;>N&#$Whav~&RtI!(_cF=x6TaFz~+oa(& zTsb|j?pf46ihKbx*XKGWq3ZX;oogp0g@%=QJ%bMzK|riw7md3hsw9IU4Tx1xuU9w? zxjGQ2N}~ifOhddAkTfp^Afg0wc3-mMib}mc!b`CIcg-_q&PCX6HiQtEB~$aX@+ewr zvCH<6>96GcFRQr2rsNoTFO9A#m+;u~HmFe2G zXYnMo$E2PZ57w-j>59|exQh-6C4?MwLqFOy=8l$^LIj@y1oAjI9qf+9@t)Pc>Mev z9kx8UzF{M9x!ZF4@|fnH?mm7@UW15##me}$AQEMH?fVJI41;B#Z)d9jKY z^k@VBdUM>qQPQUO+jHp#kA1q!nU>m*TvU2OpJ^=Qoc}& zeYtCHi-2}YLQh(R_0wDU(Ez|!qT8YYKTh;Hfpeo0yI^a}IM|W`RDm+IZoNUBPAj0* zI~oV|bl)Z=hOqH;rzvyEXz`x^u_}j(&q!?7z1)|k1hvKb)r{0QH>ygWGeh*52~i+u zC018}Q;Tg%d%s`24^)kt>cCzM9OfCDCQtJUq|qlP2g8BHl{h<=# z5zR9eBdVFK6SEzd!$g+N2ZnMl;`NG90KflBe%n{q!-H?!KYNdQyh6$i)C1Uz1x15V zWAHK7-u7cDo^eX;2{pI7{pQ0gPDwq%nsrhLY9yl}+$~Jt#7*xpd=qNN);h?yl-e~^ zKP>LP(axm_PM2WskYfwsW}(E`8ZLANfV^M2v0GhmFGm_UXVR3PEtP;OputO&6~lXc z34#&=j4`r_y%jj^{kmMMAGy2mYYDZd{!YUV_Xu3?xtC<(?tV&vaZIjUG?Mqnrkqaa zCA4cYT3tcAzUqAnd-Y<+L_7mw1W~AhwxgITqDI95&2fkv4>!E`r+yMSJExS&`EJAI ztsQ9-$x$6Reu=D?P_**luh0IZU;JhBeE6#T;2xN=a%H6yPd~>$RTCIN|D~G;q%7<$5g{7GY%URiy zYLq4v0cRG*Ml$N1gIf9jQjoOp3*K)v^{{-9TYg)}LZP`A8d}c2`gw{O5obzn$C2&0 zX&!-6YxaM4-6LTEFJtFcELcY;MXgzZg|f0&jWTS~@9)E1nZUp6X;yM|Uc#(i{=B`o;MSeTNV^LTard#m_E)^&pZ&GJ z_xE4)U4vc`9>C#vMLq1PHM1QDhB^?!Kq;B3)wfH_$Hzt=wc7E0TjG0QwXz{#MX>@d z<85OGPlZK^q%imZjas~CKHbf4VYr$HtET{A+nB!TI{Ibpv;F@rhYk*QB^0q%N6?@5 zH-wYCnzE99`sb}ruQi9KL?k(ReSdyQ9`uxD01}=Bh>jhUT3X|Px`3kcer?p!f@ZGY zmCM`uK%10C#{ondTe6haiOAD_>;36pR?p(Qk_zI-X+wuRObaWN(5VTw+zZEALc7O( zAC&nd+%v^WC6}4QgL_O@57=xk2yye~Wa-cH66hNAickRaG-G83EmX}UD~<8S(j;?f z*VSBYtRh-{1GBdLgih@Z3z|O!k9o)FM3fi;DGm(7hG{P7bZkJ^YME%iOVx+K#E#H6 z8Ui$LVzEHds{)&A+{2ER-wt-teyjQS-n2s={@|a+1rS*aghC5TMEttDtL(u)wECbm zAJ0lQDptt3EDb?g*3Jim$l3^1Swoo~l62AS9!|fOb{Ic>HEL;d7LjEZL-!roq8kTZl?1WKhEOC?dW3&!N5nGh3RU;*D$yT)@wKAs2pq2T-eGXON@qrXx8pT4d z2nC=zxAtDjoDT$!U>UR8XrC;az`Sg&HG}lIbc;o8o6@V>6j2xxoNq?9<46*`!rvK;9Ruixe`uX}0YFi){+=5X-=1J=a!5Z1GN^flI-_Io#A+TZsX8D zvlSH4n1hY0fH((XIsXp0c3`#TMBT!#6`2cXqhGt6=tyZy+Thd3~t=2bsnca`yq{wdT0K3G#j=)2B+eJ2=I9YxdTl)mSPX9VqNLkTPJLrNBlMnGfiGPrbTA zkH=5^zOVke$3LPQ`FU8c3=g2pP-l`=cDt>&eVUGK3M~sLRn2qlp|TFJz*9tNX*3Cy zJ{PaWy%JDW=4ob{CZBC*z|A%LIdhyR=0?C-%3ZZt(35l(O=uUN`ILvCAkh}^v0M56 z_EXQ`^vBv&n4L5h1-duW-%EoQood?@$4OB_u=N~6kA0e_a@xYI18Zqm1;1Zj=wvDp zidKfjducIz|J?pJEo^Atg$7%~j9OQP5t9qJw^a8x_Uhms-!I-&^KQiaICgSRIyNKP z!kivniDv`6gE0)r7#kIQvdZ!5K1B>;{UAPB00&dRAd>htZzTE$#2(%9T!!!5ol$Ky<%)#;~$adp^p zoHNtxvFFkvizeR{LSjYU7JPqY3r6)$JRXR%PV+14Up+$uk7+Pp2+q9j}IHMl~Lv4tcJB<$bA@|~RgcJIVfmPSBZYIYdBra@~T zt$LdPOLJX`TsMh+4^|6195g+|fDR72{Z8%7#{B zlW<|{mQM`}D>6(lN_G&cl)`nbSj#OFz@ji$V?Sr-x(7%Jui=jlM)g5`*8O17C2j>4 ze7zztfLXn-yooXA%$#TMCR^8B@B(Hv23m~1@iv>0aU2LOd;{~^^AE-Ap|{qSR;=4MWto|GD4xmbZTRJNNltFMA@;n?Bi(8WVzs8;nzKk(>jLOVw50G za`Kp6n+GJz^_IGpNn7^->?9-tzh@z|u*3*BnaHsKZQO-+98PJ{)+3!2sa$1$sl=?b z2jW0QNSh5|NKaPEYrpW9{QL7?|HuFQMcF#+C4}c9pdvPl7i9Ss89Do2QqxTDP_|+AOOQkOiEhrMChi zu?dY4#0XC9Yq4-Uw{BT&RRqoGVz{*m{hXVW7`CwQIvuyCNP7X2_twbp}ATd|8OtEVeeHHxjscH3W6yrQqC&SC?e+`_x%PQk}tYwhl~dM94hg|SCV zSi9lz{{I?`-mEi7vtr^Sx#yB8xfc|5emxLn@mMgwRF8h}4NbSDwOG#r zn95SbgY`lxCDclimJR5Yi%VvqolJA_!N_fVYVVIwRXzLC6;|t07$hux4c0Kl$yrsK zF!p=gCknm4a&OAqslBJS!X8oS}q1Arhl3MJrmBT52oOk;P*A1mL#8M6lLIs?V##TC8tM zXf**r+!akBc}&{KN43_53&`4Hd$HUBGY95!sXl}gZ+a;b+g=nx%Z;tVFGy^8^yaPj z=x!X}Zl1XB>opYUwCE*bY0AKrqARFge7qbH%>NfU4M*%YDRucw71oh_I~pGb^&n=+At;4Q>}r7we9{=k?LXPYf!Ojy>1CY+>? zbaPJ*0SjwpWNBVzy@+c~>lj8AA9lJ4LJCHf^LI?JsSS7dX zK7|aHDntpcnA&x9>|&W!*m?>e!rT>Tr{^}WX^1c+_mYN~9vW*dp&c(qy5G{@RiY`I z&?wK=>PghFoUyqODje_MFwf=l!mxd-qORKW&28=%q}{&Rwgw^-b?HU?wmmYLRM3&XGrOuN1|Tr)| z(s77JQRvLV7*_UQ_x%lPXn)Ja(>ey1x@InV`|{pKte@k8-a1F2w^^T#N2cRUz^}h& zPdb=ed)UzwpjMxMSgI}IngWH!K&(o>s_1d1OcQyYq4)q=oeJ}Q#-_~v{tf%9J=2tt zG=9dgJO89#^$R}vul(q{-@SHrc>&hTJ_GU#H`Y+%(xf!fk{Ma>(A1fQ!XCw=$Y8Pw9+r>J?SpLQ+MdJakm6e0!h z>lZ~AqESyR>Jm!OzV}KLC6-LenTT-AnYsFP%i#BXQ5r%U*xJH230QDNq?mVeN)bts zs6Ip%RfgDF!JE&3*o+a`oUzrwwtx*AHn*POU;{TbbA9hQN*cao_o~~Z`O&%@de@WhSpv3c!oKOJ#z z=bmy)@KhpZXsKBBv|3y^22Peq!@#f^2_d?7F;7f6Qw)wM``OXh&AFA3+Mj5FNCo`uCrl zjpQTYq@7>Rgrl(;B590ha6?mKB<^-x+<7%;w_i<7J8W~w6t+BfC|p&zc-5<}U-Q~e z`a5s`)^B>jl)%g01DN+WKc`I7Yjk!uuNU|FP>9aV=`o|W+Vi+pi{Xy9w2ipf=;pf0 zk}T(>8*D?0tvQ~HX^2!zmON%qB^h`qYd}Vz@!|IkFl@;k<5$gr4U(RhII!T)( ztOcr3vq5dM3P#w*$Y$Jl^Uju@1qV6>LxL0pP()kGfTt#Z}`=&;< z+Uz+@HDgUt2)39_&f@3MsJDP2 zWHpwwot(zq(i&{|21I)1paE1ZzVBRfgQZTs=aRc%K3tf?!X@b8u*%w+yz8Qeo@Z4M zw#vtnukLdSdO?e{4Qh?r8gWLUVXe$K5aU2haq09^N@*PX(%OaN-j4_=`2OzJx?i@n zbY7{Ih&AS<5Vax-YORi6;CRT?h&LdOsZ~L?K_s=03bW<=nXR#WgyEd=$$9JA_*OgIuYE{AguSZ ztE@c)m4_9;L&DEP$pXKQ?x1wxXQdQ(GaW->Sw@GIK7~gs+PD#=@h519 zkml}pmYuY7>lq1EWzLx~2q{L-<9CgQ=geb^^Q#GQ5FcMAZe6Jr6CV-&{;a&dM1-_l zo3!sDEFqDft1Xq3{p(Qk4$IP*ioIKqyYCHaY1x$|(r^X8$)X;{z)9;hf`^Ub^kqWu zR_1MhjNk9JUuuOpqf=%#Yk- znitTN%CeoC)O7d$RKTm8gaJ(roc0x5-ggp}0IeA(YcW%crE6(D^kZbZ-7uzsnlsb! zh?e3OzgDW%Mod~MwSFDV5~>kx(VY!gTtgsLWvT@kBE!TusNG>+kVJ^VXJ2a-q{!jMZZu=Q_9t(HVhDI(AVAwqhpYK;IBO2fFq-==~Mj zO%By3GMQnaf`%o-APBMHIJpNET|28{IeW-+oMBVAymiK;nPV>0R(tMUh+f#l+{;1% zeCg+Z>Brb)KmX?X*-zT-wm(qvk&$`;D`@B-pDbjUrBNr|U?kdVIcew6V|q2Cin9!zBhvs!YiRn3Rm4xb zQD)oB!T*oFca7C{TkpDl<8^t-ordS&wUtUTz4*!Fo4KZWADfkGeYJ7>M>Np$s#i(h!Y4amz zN^-+KHHNW{)+!OV3U27{uttO;m9k{K6UzWpz@#!MpTBXuCsUDWB@p?&9tW00qa6~7|=oKz0jI# zSn8>H{aSN7ulCrS@%P?&tDD921+vxl&QgDM{B2~S_NQ(FZ)b~@E%NX+XdRiXc}J}( zrXe~;3qc!g%{X&fW@@bpNe%(Jj=ZQ8nh))Lly%PEgi9JRNu0MPol>YDN^Dgt8RnuR zHwf!zplNgC8Uzq1HM7*hVogv9`n3X)?7apnpM1ml*CyT7P?m0GH+u2lGQL*37y9!L zsnm{hQ`b2mn#Ms3{Mz`H2j!b0H>(J)=z|Hy=Kx#?IFZ#Y`Jq&N&4n^A$WjOumg9)a z9-ovholBwkphT;$;`K=g0Pgv_?|ABhn{L=9AtG!-Aq7uz;7g&53uRdt;z!DEZPeLu zpiDEbrzj6<>j!mZOSfA}*+4Mv;e&ec1@*zxX2811&qcX@zu1c?rbM6ZtJ#wkVEM?R zIH$o6?Zs&=O7GWNymil2=h(Xa&QXdmO%sb>7`lNjC9Q{-g&-OeIsL5Zy;pYowJ^3dGVET~?TJshY)jXDZC?UWn5G-VBW2 zL;wIF07*naRO%oBjU!fv$FROmMK6}ML{qr6tZ-##0Fgj$znfhfwQemnQ~T(xX6ACD(34phYo=7__B$C47yl3l^yOUN z^0{yQx5sh%ogv?|4-2srd@V?xD6W!%BS=_E=|Ohjvc#;FvuZyn;sg zwlL4r0ZY9_Q{LvoM{Vh+;C%sGSlTt$=YM$GKSZHvK+s*drmXPMbHY;Sm@xEidKb3M6aUMz!C` zIWx~Q5;jzXahkcyLg{-pL-&`Wt6x_OuHIR)+Q;lWCi!V8wLatF(ZQH3?gur6;rBfQHi_om}+9plGk&OJK z6E0C~xU1*>fp>O#HddlG|1uT?XdCh@O&;9CFrcMH3IQn8MHju8ibWF>jmB;8R9x_M z9pqd&o)+$p)_E2UOx0eS4~om*^pT-U1g8zR;5@ljb^mSTo?@=a!4X1aUKYF|<-CD@uiEya5;VKDIKfR#ov4V{Up2$bCwH zsUGD@mAp3p8c!jnWH&+4g;Fz4pzr+3GjMof=%_WX+=>((!PZFdfoWcF&c5fW8MLVV z_&CqRX~K7vyXC}9@%Y^(`@{YRtFQmo@A`ee@BNQ!zx>^<*E#^~wwtdHl+WZk6KbYT zM_fMPv6DXre9=u8ozwYla4Wt)KcM%DHPO6tMCa+c4qY(i0l7rhQBxm9HT^~FVxKvC z*Q2LMa>jrJJJ~v-1>(@lb)_p;qL0`%N`RZ({U9a^_5grQJoQ^iqTQN2Jg zL)7z3A3&+PrhAwxc*VWfZ1V`)Y2wrYm|V@~TQLOc2*lYVAUAT7STJ_fpg^S|2D}Jk zu9OV9jbYt`W~*lvO$Al^X$nOmXJrG2K4Nr`zO+N6+DG|-DP zYB+iy#d;p?iCoobfJ4nhY88_$2Ok82jt5IA$g;4w89D6PJbFafUHs&C|JQ%$N54Yb z;>)^T^8hg5mWdJ)-Ukkw4Q?(_GeIgYo4&Wx)U?rYeIuRI_LW;R0NtxXh=CLnWyzN2 zR_iW{HIlpnX%IUF^}%9jLY-$(jTJ83L@GSW=@(~$A`Lrm)&PK(C7gjg;w;;+SKqRW zuat2z3D)?O053y)d(w^r3)tVInN~KJr;V9HbJp=_QWmj2j z-0O#q{_>Kv+5XwM-Tt|+%#Qf-uh%*NY`YB%3+`}1=!CsHL8vU#NG&r`vW;kD1=cL0 zM2{jSdb#!i^glYn)3J;La>*(=)w=nX()plShVxM|=UEr@YLd~8d0-XAYxyhZjqPpI zDOA{)fr5>{^c*f?o4}C}8q+Hd5q6X@D zZqow&YC@|H)+TH*eSu)iy(Xgea2gU?&}%bYpeZr6+gpsjEDzSiQnkF$7QF z^#t$8^FnqrQspdGY4Fjuq1xmDVLk1g*lgZ*X{&pSoryYsRy1B~fH#4}EkD zDOjda4CGUhpiRK)twgDqs%<9Q+&xP}8~Sz7(a?w92US82jwqVexA7(RGgu=HA2c(n z3V~gXLor-H(f8+_Q|vufpPK`c3n&s$lr8C_W;By4CP=+KkQdT`(T1O2DfzF zH@v@%CR-`d-luC>5JoO)xr5W7CGB2SS#bmp8@;V9t7nHsC+)FBVhG@c;wp;>Q>jcM zNEeCw9br5CwVLOD_qTul5B%^~cw2l~*J~XBUOj)oGN}afASc47=HV9~HSc%@e_!oRy`9VxSW5R0qEw2BLS&DA&D8S!;V;sAogb zzcXE2vAWno6jcb)v6+f@6p}p6io`QdH2ir6QaWgtZ9Eq|aXp zEJampiUTo4-!)aR7wLm;Dh&YIF9BHcLdjX1eok<;=;O3;rI{G`hO`tF-CT@65p3J| z1Rd;o?Gej~~4&oc^VM=a2o+ zKXCWp*E#^)-rh4EZ>gtS4t`-%7lPBKQx_86MXlM4)Ge|=w)D6>*!(84fx%CU(-SA5 zV<_(wM=zoTk(5G1;;bViO(pB9`PVkEUV4(RTT)s-szL}>V?PKCD-=X`P>edBOGybZ zL4=5-3xOd8btbmvotld@mmYmG4FC|NGH0mHnpqx$=Bgx`b51n^M8}_cqFRR10+XmN z;Zl^+3l-;!W-sfc#5R_10js3~Eqii==x}|-CrB|6eMH0=XQoob>5u!UTP$dR6{r0c zwQbdFsnw<-`lgf!WsqK{QegU-KbRH@u+2z)5c{{!A+w@(aYGF(%&ZNq+okAWLK}^H2^V;Q)Kop@1jy`(2 z=(J9C)@gTUG^(psdmT=o7frRJDfo&*d;q7u2jCdX6wj+Lp z!=hrP!dcJXAq1%e6tB>wMC=`;jEkbw+K}*s)%ts07M84PMcV8XVqgUlA*|)g-3+6z zbhVPa@*$EqdaueyDF$3n`87lVI@PcR6%A|^T_eqXxY`)j*?SaA4OKbm)ZFVvDr zG0?|EMVRMI&IRuuxER&KX4jE(X6pkk1QQ1_|GNU?G&A00f`GWnG|x~oOVCkesg<1J z?qqeiH^P%CDCnYupt2Rk+l|r_Ex2#Ax`Vs9j#3Y0i*hu8=cw#BqboCiR zF`TANHWpy=Fh$4Yy^6i!=+SH_1W)G!DS5m(c!<+_-#b?6FmOblX!dB@r*P}&QdxqM7y99GCIWrDeAbE#CyyqF&VWE|OfvQxrMagN%gblTC}(paxYP06}gKEenr zMWLy6rV4b`X7IMyqIHN>%TCirHPSHJr#pWYG9s!_pr`8D*1EbgcxOfbJ4>-mibt%a zDWWh~RmRczJK_jRq0xm{7OE&mB1T09hN{nyDa^9ar6CVx>&UkTaBc?v>~p zYVC~H5^JgVdWNq34zUiVtYCXI_!;)t*{D(537!}=(72+HUK^MQ%ie(5nz`PoD|19` z+EhguomRMDgd&d*iRd&yRIgYA1YFi^fMS%Z2c-;Rqhcr4-EY=xVkf_eycn3t`$`q3 zFm>C}Ic=L6Kad4H13%ir#)KmuHt4`qa?D2DJ2H-03>ZS|`dibF^X5^d z8x{FnL|CevZM>X5BLcxkf~oU*Gv8?77v1nsobu{VOJ-WCN>x{wOJ!OzPs{q9gWM)S_Qb1m8xm2JuzmkNOI_pgsT%8X#@V zVC{eJ&$eYg*jUe@m=bsH3+Uv<&|oyIgnqzIJi#YQb==-h?%O}}_T@kRh^aVtplv5asd{pWRZ2&@6a9O8Wup#r)cBEXWOu?nddCE z7IC-`32HHX7RN+ENK37pmda8qQ?8tr!f6qvqLkb*7sk0VE|rXy7`arHKJx(=B6F$S z-W{3qgC1mqBianz+eD-oeqx=M11ksm0nD*E9960Md7HQ=A?V3V%dS`L zTu|mAjw0=|((z~(Rud2Fd1CP%2dipO&1Ll+1Wfn8t_sk(oF&><@m1eu`F=h1-I^JQ zH5OaRW0@m5ZuO$7&)&MN%ThRw^B}4J<@>kZJbvdleA7?-;O9U858NK?wJZT7XQWh8 zi0p<9``r#-JmXE&wA-3e#f&Rk>YhR8<5u%Ei7y)f=;_~@J*L^E2emuHbKy)EweENo zqwH3lSqIwh`gI|;y-^)!A!5Pm#|M4`=npGm&LJj!jesxq`=V+_BAF&;zsy36Mazn; zO-^lN6-}KDh1hu%-6)!-XYmk{mLJlO2Q2 z<#(q;H~r`T!f*I*f6E{F_%m3hr5FxprG`eBYN|F!rTUrBb^2yf0Le(P z*>})pAytrEsI_Qa>os^bWT5w|Lm)PBC{oYdf(F5DbN(z;$!m7tJ#?W30H>}%!IMKZ zl8@f3V(+0@okb@q@M`o)XOQBRo8Uo$qXbWJ9;r%t3f8D>$$)d#im(zcxP~Ka8CG2< z93ZQiN=442M~m=cf+AW1D5y% z?b}pma@!VkeDo_BtLYfF355&FQ*f9miE}G|0NcDVs2614b;J-9_hD9MVts|uz)>gy z6!=K?(;SZV+Zq^CQ2X_!C>4C5A2x&-8Rvy=vtxMl=+|`J_SJX&%J29;{`iml$ntUR zm%rEbT4n(K&{H>C!aQnibw85ajKhzqQifo&*`AbBYA!sQ>KB_tG@58eSkQ4+#@X+m9d}DD zYxY@8P1m~zb*fYB!P>J3XYRU|C;E5abV2j6f}Xfu zoQa$i7P8j+v_3|oUkkzMsjL}S#b6GEps3CigPrKszc6TTrI>bsecnOWNNsRt(@PUr zY2z+9#+r#zv<2)PRSG4K_~03Eg!?<{%NK+geitbmfbagw?2a$*dMyLM)uYGsr66~= zIE1CB?tfYIgo!aKC%_k^jpDq=OC`IHK2}3;oij&0HG()g?-;yi@Luuh6r^Twl@t>a zJWDNz%a;DPj(O7x(2jekT2CWl%0FiVkaqens4evhPnJqHTz@n@tR}DhPj1HeVt{z_y{0;McB?h%ibN)BxIW#FNAkr4VaX+IaA!7@^h`678%%;O+DC4Nzu^GOjw#)J`=e zStY=NW;Ct{T~G#L$ptr7dgtl74f*bdf-saC-P32k<-ho?|H?oAd>r}G^$oN4Y%d=H z{HEXkec$(&zw$fie7%+>z%TjcU;MwQ{B6&*@0M{-!uosthx|w zM7HTm%TBy+rI5NawD~=FTnH-Q5*3YEDlFOFpKIerZS)m`B&&dndJg1LSZZGHZ?Qn^ zL<0jx4)WDt)LzL((K)Dvl4o5frE1^E=)9$ot1w)F^M*yRen-{&t^dXXFRQp|vC^kO z&9&{Mr4kift&FGB=J^L7JbwR+?>@e}xp^$}^7#Jl=5dPg+rItVe(_KK$Y1?yfBWOw zORr@H@B@G12ma!3`u1P<-Mhp7-<$Eja=CMg23$rEXW|z+1!%?=)tMEwH)R^fN5*|E z1JGtrz?VXDjv)lLDba}%b|h8Rt8eLE8}T`-y`47nD#F-6D_J-r2{`Nj+i9n%yq;Xz zP}I{M)@JtEh{NkFJwzqH_^4>VU{1c`ye>>t)zr)+SIRPz%VMfX%0CEB>)({r=Pt!S zaWHHLF82rKaVF0hL>q~D&Wy*An-@3coNUvR(BMAKlzqT=XQ*{?&gysl6SR>SqO(3g zU73Tyo3gLd8Cu&^EhF%tsYL~mOra1YS%Xqm>2I<6q8fL=JHK)f#7Z`8wpK(G(rrow z`k7e(X#nU}vdnX_45QUk`ggZZa4ZY(=TT()tOFJ5a&9c=2|34NL(+mLMuYchuKL5*p{8q2ticCZwLWjh2DF1ea|j+bWYR3=uCmeI>fmsF=i3{s@4sz9_9*@ zeJ!F*FHQIDPy2TxB{f;_YNGcc*l0{(NRI7pV1L+ixY)D5*t5Ghus`hBT^!gR4s5nt zy3K}e=ovPf^(%FW6g0){QzAue`o$R3v#v|@eIkTFE(^nEV1L-@a~%$BcUwYAlv0_e zh130{Qs82EfB!)I#9ns@rswa`K$UEsFWQ{+F_D9#w%;|u0q0k#5rfy-4R8GpuNxOp z%>7~-h$<^yKjNq)`Y{rnqj!N$9Gxi1E1|M$-s&2C`QE6@!jh}mfU}Ng1z5_1-o}IP zx4$<{$Ld%ar)$rn^=S~g7+0yX@pNK5jl>YRy1Zc9_Z;uRPMXpcL#tJIpeJXik%-Z8 z4YX6$!B&N7bb;lPk+LY~J!p7uWov15E7|~5qY;&WT5NO6`RQ~K$b$v(uqkO}r49_5 z@{nT~68pn}?O{*9*$`5+vIB6QkUF)OlC#JKqMIl$g)+|cvBMSZV@OpE;t)M9MRnYD zf=^1Lj>A9~JEX!qF0j;9)WUZ?Uc8nEPJ0?E>>*k|x0!v_nm?t8#7=qoi`Ln$@3cO5 z=ShrKH&vL42-K3{#&6gRoBE%8&-c`S^LPL5zwnjZKk%9d0D$lR;Xm;|{pN4|CuebPOX`2A znt)b2l{_jymo2}nh=R)3bR6tvS_wcEvSdnDTjzP189nMKyG#V{NG?*S6sJwa)xrxK zuT|q2_y^IyEhg{Q#nWDdCZcPG`aqXnO(^%nKo=4&cFKwgkE$#C_$O~B|4-lgzcUo2Sj@2 z)40N})rGgLtmwJ4|6X5Zio)9D1QgrRzoY8o$RuKJp#bc;0Va*va z`EKVl+gJ@nT7Yd+yJ~|To7#k`y`HlwkXy&1ZTi2mFDxr)P~isImKMD2Au4v(cz+fr zt(nH0Y=NL!Flfrm*%dVpEuPKxz{Qg%9G<>G*lrnLyhQQ{ID9D#UH@Hp|Fgh93;=(( z>%ad?Kls0V*DwCIJD!Kp0UU04; zl9{E_`;G~R6d?f+kQi~>NFRFQa3FL?rsI+E{+4+}BXKSFH12_H!gT*?5F!~>m= zbW({T1Suv4tC@pqBS8$3Y#5MLBL&4Z6wV1ou^^!R22tf_V~Tk3N&o_CP=Z$rAew0i zQ=KWb&~+R3mwPU+F7U~7cYnkE{Vl<%&SUSLmMis)MfgfypNIee@IU^QANvcx?&toZ zU!P+CBc;^K0^W(5OXPwg(mVzYZMt!+8&6@XMhXfJ?-!L2cQ8vKP*sE8wZ4MZ^2>t& zP+ddexibjHV7{Ov9w#=I#Gx#{a~M+c33L%QU0~bybg5&>nbZwzcRLPOms~!6!o}5N zwigEt03niVA*2rH9CKb6$C1#h(vr4_GwEx9*LWd>3GL9-IHgX#Q5NdNh!!aC0s~0*$@bxSZvwco z;DAl%34LHLGq=z0_~7P_+i51mz>p$C^dzTlS0Bgqi3tDzfAS~(+E3TwNO=GNAOJ~3 zK~(sA4a2x)9*>meghR`KN+I>V>X?~KaNlhh4j1&h z9X=-Z7YByjp6zbOrXO(Pn8%s>yE~3|cjVK=&8uscabiB5bn`luK%1wTdDO{|Jp9}r zr02mwAC)f>#Su{)K*XzMw{HM$z1+k5W+k07U{G^C3iuT)q>4i42}ltkFG7zBgtTdZ zIJHl+ezV!VX*D-fxE=4g&4ruO#Pv8ad(YwmAtaTy7ty)u$8de30stU8xRbO&%A~W_ zw!y7g;~I|@hm+ONYpnsnQ>2ny0xEM+Ijt0QEw?N@CZ(XxoWVQ-3U1Ney^gSC4KkLA zxN{#sdHF%>XA3gH)QQ9geAn^DTc6?T=^H$I>n*nXJ-Jq1y?V*@-5sZCX33S)G~#2T z>w9*G1DB63+3$A5z9Z<nhXC!0n4`#*$f%3whBwUf(A+DIyf6Ipf5!gi1<6h*f2> zB!E|q{TL$7_4p{H!I7uHdiO#dTyyEFwZ*p<d} z{y_iug59G>Ts(fvupP9d+V7B*sKAR4KIFr9-(|czB6DH7AIamwes`cOh3GeQ+sNYX zspCD4$}mLA;yF!_Lr@$*LlPRqatN^To>()R=oz9X)}WpdjWS6!^rvz#eDZ{#lay7C z9Bm>dLr^)dsI1j7$~%0F8U&U?393O+(b7TjQguBB8fL%=#N-&Z9sTaW5_)c@%<+`* z-ImScH>ukTZhd_J-~Zn4{a9qgK5+ryk3r$D{>4B08@}mV?z|z`x)^aND5&*PyeN$H zoWQmJh^6@gg%wx7I_z+6GOeknJAYrNUt$4(l?|sUq;|Avv{xHxA!a@j8qi0rzBh+Q z9G*O(J8aqPww%hu`|rNT`|rI^EyBhAg6(EYHjTT!pSe5UGv~}Sj=XsOoN1nT^!SRa z%cq2)XA>es$7w!sdw0!poH!jv6&uBqoLBNsP*Kn>MK;NkqP9&J(XncXIx;0|uATa1 z8iR?uDf8j&MBH6+@$}Q|9zS7oaUkvYki7N^wmVMC#PRNyFMjZxyPJD9v1fm=rArIr zaYBfcypWAqB(6ikjFb@(HAB68v}HME=2`8=z$PdGcD-jmBnBTy6@n;}(p8l}tCs6~ zbOz}X<`t-qQp9uvYh7v5VxloPw<<-cExDGLOMu1TkYe{=frL6L6**@BF{x(YW-}0n zj->{cl3Bz-H<0$1WbuE{SN_b$wO?LaAkg3M`o_!01j;u#>Jw>-Eg0R$S@rLfz(OrL zDxx)Snv0No5EC9qdpoV|)qcU5d0s7?Vf>#kxRBIrZpJ7Oe z?Zts-Z@BF zahewHPa{4?9$h^FA}I~3HBc)x7v^bVI-Q_YcEfvY7cImpfZi7$Qdf}7*Q+h6s)5q*>L0LUWlhKRI9(_Hf>~`qD}PkF>eDeSZ;Wb9xo{ z6tvTyqMCVhF_65Tq$zr0r*(GhJFcEQ=B-b?&7;SU>9-p`eDRW>`NBKg-%ngzJm%5W zGl~S>{otB+-hIxyAHKxLEyNBCGf>1aEtw*YuG=6)x-{T$#1M$VGmjJ3FJCfGCoXn7 zmg%H5va=Ij3R1F;;HHsz8Yy`OQ4~&|CTdok#WZE!|L_IxJ%7pJ>M7sw^M3&sPo6>Q zaWN56N9+=P-&0EFOJ974=N~?2nPwheJ!RAH`O+6ZgZhM5 zsS09}g;Fwe&Meu;SEZU7k2VX36d6*&;mC_=LzYY~nYk9`T32M|0tz8_C=}TsjJesU%da#obm;fjY4$OMiB?z1ov2%7`0-~Q2jJpBjX z`=|f(PkfyFft%gzz&6@%DMSua|WF*pE3U8&v4d6JnJ03p?jCXgu_s-97e|yg~jkvlnPZjdaVhza<72r~fN@&%fW+*9m z$~;3=fsyOmBQIaRpccnxzUFIr^7I*t56qUZcKG=ZA|X$U&WQRn2k8pK+xT5q&BQ5IDY zuB(_z1?HtvU8eLsVc4?mwqxz~e=aQUxBdP<|HB`C-23E{7y$fl{+4h52C1h%Oby?p z8&qhRJZGm{L+F7LMO2PT)bn2InSCc{_Zu7O0IPc}=OH3yYEe-ictr!+&$O(;0)R+E z3l_u9t3LqFBLy~n$6>!?yV)Y8a(#2lhwnefhn~%F;ML8@i|ZE<1~!K$95zq!U9Xt< z`&&*Wb2~xYB+_ufMik>7(w6EXrOf!?*=~01_ZReC&#@GyapKXwCrF{p6QyR=*iV5N z8_6g`42;u6j$u{9o#)KW^$p|c#O0H>`ShnhMb{6UvH`D5+i$6`%$fV+#4;7O{f?m@ zxWAwHnRnjj`nd4q?XTrCpZz(^3%vLK3$CY$`+4DF<2iJJ=sKz&sG(Pe;hfF1l@)MP zMFYURED9aY%bCF_XV6yz`b5EO$<;!RQYg8aw3-o-ykqE_d8+yW1oH|kwNMS(?1Nh= z(Cw>@p_7lMXX05*ZNP=Zusg7Q^z`U9hyU$+{)a#Hogd@=_{jB%3jn|In}5}o&i$y* z@sZ5x&n^Zlbq&=a?F82w5u=iToD=hpR}J_kb*-bhfDeIEm3SidyS~cGayvJT314z8#}Z2%Vf6zQTTSD_1@?Pg%V-7s{Cu?nwVzG9g&+x>w#3(sHOaeFFUK6y$Q z9`XMB&-vS5_$dw-7d*bYV7J*aF7WDhhX~gAbNb1Sc!gLw|&l_**xN^!1J`VKV22wI_ z1#P}9rPB8s_Crq>dVcn2-{ZsQuh{G!@u|;#BQEav8~^v;!m}7QxNJB4us(w#?AgbBUl0LFJt&H^t#%z^9IGyCdy(f8}@l(LeR8zbYHWu1{P5 z;2G@dhCCIf<&ILb;?%uCZQD4;>B$wmGsPQp;X9)b8@If{e^XS$$E6fg=~1Lt@Xiok z#tpY|p>xKFvrGbXAw&m#f=$$~9@uYsVvLk=BKXMADFRTj1cf|hcH2E6b>v)$-G)QI zC2lVG;MF~U>!-g&E|Cv^_I(x!{K9YkG+*~MZ}aTQfkzixhCY&~i4YQbnK>TssHISf z;9aEe2b^=<-rR6HY=}|_wcu*ON%QA67aUU^t;=E(TzW*#OQF<^_nyPW1)J@TyIe`z zf!Yt8ifS76{f1?ksOV<$;s~JwA};jA?t+ED=l|AE^Oyhn-=MmVtIG>^y(hWKaM-fD ze8m1T@@l%_`KxQ5?6*2uF_9A$6sfBSDX9HeMZe$3R~o$96v#wMTG!WZm{p7a^k|9JpufSavjc_2{OJTlMH@nB7Es>$2YQYNp!%Rh6$65(dalYzr<4pWrYJ6t z!hDAW+aa(Efhe%wZJ@$9nx?$TPfN*0h7lfJULs+`EFD|tS=_*s;r4!Fu8xp;US7Y) zZgqH?ET)>M% zq?lTDK}=9m#IY=y<^G=X(FpTZxjs<=;MafKulejU`v3Ae-|l2NQu0JD3g}T0%SbJr zrDXC_S6T43ah)k#cvGIJ70LoqD#2$+P)%w)_-Gwzu@d9#XruO0g3F7}$5YhJQHyOP z?Kh<0IP?RZcci|foIS?@q}74+wG3SVMm^4$QeH;OSy-ro8F>^f4gw3AA;X-{B(J2JJqw9K;Ir#y`G_xl@Jg+=Q)0K@@$A`C-u~3vyzwaE^9?>lrd;Twg1RItjo5i1 zC2Q;zZF&Yl|3n#+!N5N%zpmZ;s&az>+BD9ZEqbRt1u5W{Y@(l_&1G#YKI}h)U=(np zX6r$t&x@f?LyC;0GEF1J2Wq#W5R zxD}Ztt3G^Vk@ajw{$hIzuK|s9pnANyxig>bGhF#mzhV0E&EMRo=%*m zOxpKQGuN+P@WK1-6=eZ6_cyQj@VzgB6zcLe``w0IXQsPb?yj$?%R=8J z(+5oWT-Vgt;qW06gNkR0Td4PwE@E>caM1UvZITEyv%gTX7ctLoHw3_ ziM!}YEGrmC1GTX;Z9L;prsrIB^jC_W>MnTI4zEC+S0b1Suegthg6Nn~m>x`-rGbK7 zT}un`+Gy;Cj$ykaJ-TFgd_{lt?5nj)*319>f8_PKV&EF&zsse}rKq^QH=%~$4b^3E z+}4dp1X;x(b-`-G#~MPu0Ymho5}PWxs$JzXVMJEVx~AdhYsKe+FUmvjy*B=~$+3-r zA$a=W2vVRdI?8fFo+oZ^uerOsp_I&a+p*gZP#5O$j>y8k5A0H9a1)n3ynVId*&(ru zg?(3faxw5|m+)m`9&bsZa&@sK1=u8|n&#=m?W-5eDg;_ETXborP?Sj zBvLn!QcsGBF7@=u{%zg@;5d!bDrf%C?MU?rvl?Zj#IW0NKOT8?`-%`8&z@cJyg0pM8U`dE*gR+ki~BynOE+-hbz(xq0ycc{-7&5sHeD=zLurfwgaA^*biu ziEUo5Qe?%xs`J#~)oHj2Q>o1LY=JH|t93DcMBPU%(rnsefyW2+DX7}IStmnFEzDBL zR0#MnEK2b=(pZJ;2-@Y~Vf2MTxw~?p8OD0h8 z6g`EV#bY^xHqK)?8v#;HdtMi8@Q^~(jRDkLD7mcA)Kv&X%e#;82`CkhY6(PN86s>u z&wdDOQ(zMU+peQ5<_RHcC!IwgMy1?#J$TP)nm8SgjHlyj2i`>wWu{C=A{kdlx=?v? zx#8*6juZ;V+m}qITMqjzU-#LsVdx?Wp^PVPpMSuM_rAz{f6L`=;H@W5n8y>Y7PVNb zm3f|7mPMxkA>n+$`G^mZE+t}2xDd$#(^AMjvb}o3)3-m%L{KTPt~ci(-6)}<5T_G2 zcdw|WFbq9Iicm7s@t))DHMcLHr4^rju}?e2(;j0!p+UNZn2^lAC82A!(2=kP9=s!u)7d|T>|rk&3G zxtufkY3~?<(^KDjqn1`&Dfp_=;xPnval>go06T7d?0l_Wt`S&NAQV$vtgLjt?1_FjyY!qCk2mCkZM{C z-t61DgpZz!t4HM1iE+Hcg~;QJp386eEFT`8^ZCzzft&01xV*gNa-!LN^B1Y- z0MYskBIjUI^~3F5V2FVfh2$&Isb#e{d6*g(qJcz6k!?uYi4Tq?FXTMa^@)pkP^l^r zu3z0S-M%2!MbUhZx44kl?{_%wInDRnet5_IcqDq^aB;zt%RS_Y)b-?X>jMy%*f^)9gZY<`2_tDf8r#dZYeOng*( z2+&s;$HYZNOQmA=>IyOnXU{~eA!ykH5su@LdOR{NFiSu>kL1Ap_#smo z{`?`vUk?1p$GKNNaRJ~NkYhkfFuE@^(r;^ch--u1hQ@3Ea36sPYnrcJ?wl3m(m5Tq zNzv|eRP3kPb=ml@ipoUw-m~dCh7<_F%8^>|RqNE`oZ4wcj|7iTiI{py5pHYcbUJeV z>RRQqVxmidCszmRGID!;!~OM$1Axn|SMG7xG@PNca5Q%~m0wRf` zM2Vt2AaLS1!inOL2jPVyU*!!PF3yOT650tGV(CSoU7U*rooim z=a|{rr@E?kt-be}^ZULr{^Ng{&Fy!ciUe~e-US%)!1L$Nxg0J?dCyX<|Bg9f86Axp zJ5S$u#=IOk-QUuCWuA|`cl(_8=SJ$n5HpVsmn_RYH}Cx-_b;CF_|bvu!y|eYCFyEYXy@z09%2OxR7C(ZZ+8qN`MbfuKSoFZdP8{^O{f7Uj@ zXG-eCF9#1LL?e`Kp{sTct}BQsaJ?TP?&-3pGV(AhFYoTCb$b$D+x4Ns002IFe7An$ z`Xm3lmgDaY+Mb49k#2f>b9V8OmXEcfGx|&({0ccpY=K7OhB|ntKGN|qpmRjUee>{< z(rPN1-Pm&&B1t+?8(CmTfiXw&n2cha%_A=i8OfR2m1&+?=4G?^77?CYKjQlGig6ei z^1yDlBgDWwPlOPdmx6ko{AHKMQe$ZfTnq3@~CDaYdj@4fpaUc7jp+nX0m4|iN0Mn3-0H`xt=FaE-3`NH4- z92Y}iaL{!HVQndPC;WKWhu`EL&rVJSr~!J$?jynI!reFnL1axmv%|y5KlXnuax92d z9%~>?Q>*_&@_>^VP>(-rUTuP<9<>I;kU3l&7=|6%?7h&|xO=!Kf9#Fl_03=ViJ$%1 zx8MHx*R@_gbQ=Jmzwq+icitQh|4t6!qeF`3yyJX;ph_0V!Q;Mr-~8so3L>eE=!_Sl z-p{dtenpkJHY0lkGx=25&>m3;qU0emjENx!JEZ~)$xJpv^wKe9N2M)QxLZGB--8OY z#_jD9?T~_10(sam?hg!yOY-58e7K;8#Bg!N;rcO;o;>5p(>Hna=qY2~QI^K(_Kwrt z4X4`|%=foQTZp})Wk%~vq>~fO(*q$`(&18OmSv(#BMpgRNW>u^DG`T6^gh0$ly21E zJSKMgf$4P5xXbK^Y(bw!&9lw4LoJ1Ao=MTl{$UvD3a4q|Zkm~Dqld`iMx#$2U-Pjy zpK-Cv9B*H6^X`|3z4OMCM`RSyYQ7G1lDNcMo3R+h-uDnzWZqqU#VWNmLNtcF!g}Xa zAB+#Og1ZugF6Iwl;GlhG@7+RGn}VBtgk<(|rI{32wn>Ot{0)P(ZM$&?t<%a(q!B3; zt;CDRZ-fxP|L?r>&TI1n_|UZg>lGw!GioJnrM_TFzu2L+M)NXD;#vQ%y`5#doJ+eZ zO7R%0Qaf=?>+O~qrzp}PdF!E=)N^x$S$otNjWi;lDbhSMF9cU2ig1x4D#9Xx-i7)8 zfp_W`xV^pO;`%YW%PaO52X-NGp~}@4mH|1sHZC8pOgIa>Q0ECP3%OTzB3$OgsEzsN1vk&% zXE`1jg4hEbErUU&Z#}#yNY~ASm0+#ef)laCoP`CU5e;_{3jtl_;^LeCI!&kwrB_Gv zP5P~CC*X`j#}lpJ*Gla+N?o7HD3MCDD(&)$i_7nS@)!UBAOJ~3K~yV(C}npJ<+ZUAE*h@o%%>n-!o)9G+&=<3)j!P#8kH{X`N1|tWpVxQF)u)5~F z(Ht!F7h1_d;?Y&LP=kSqpxee0Vk9IXrsRBs6_FOA3Xy<_E#{OmFCI?ZynD;di}$&B z{Deo(-sJMp6VkXtL*%gAIT+O_)52UTWj2T1n->7OKt;bVINrVFa-R&43>I36nEEh@ zQ0Hbmcdc}c_>+fBOo==UM&pexS$5n5_fL>uq#(SC3feggnd35Xx_v>)JA!0n=yXkX zqNd0=KyMYQozho_3$FKj`r}8md8U<#+#9hJ+VO$o^BZoTzsK?3eDhOIc6LX@R8Vv5 z&B2tN5`pJ-mZvcU$o(QX*Btnj7 z_gz*3hq6gRx1quMd&dLJ=F7KwrxT_Wuov)XXp@oo$$y>)$J(i(+2_YK8rxZO(H_{9 zEw_S|#mIVSV{6C+3kk&T^ac38IQ#BASD$^+wWPs#&|P>a8DD`cuBYSG@Qr81pnrdo-+ zOHvT#hXQ-aEW}`)#J1=Y+O! z*p1{U#_xy&AwaLm0KI4`y;v(tD74zpCTK9bpe=83o%OG5P_-ncT@H*uQYH1ar4%{A z)Jh0{4zyWklhx$HF+{hF?^{NMf|-c=6e@_Y+l}n^dtyw?rEw}9#46QOi^wpN^6&%u z!`0_r=Q{Z?ZUFK3ebYC8OP%IJ3Z3XB?#j?JEXWpNj}96IHU%E2*?7Tb{~RQ2RReCo z8AGIF%D>uuCX8<)vOZX=ImL=K+ofAOVlF%8Fch{8XWIxc@UfjH4k<7`vPC{OWjWn( zce+PICd9VNM=ouH+kICae7b9l{Pd5ew z1M4!Qwc5r_=jE5)=DjccBBz@dEX-( zTW^H_bqsLW@3_m`K=<`Hh7CY(H+@SGi>6IMVYh%t0ClOUN|EQCOmU__km+V$QDePJ9%)7DF1 zs-4qPn5u=Ic4@Tf9w9KsOdc|$I}2;G5oYgwivS;^&A8?U^E|Ok69S~<0h*qk6ePO8 zU`BE>Q%@JjM-{Rvb=3lxAi{hUN+nBxQqZ#4_^&pOrx%>=A9#5Al8ft0p1$#jQWt8o z$|L1uVBu+EIz7tbmaXAhQQH)HLEL)(s@1wS66^-9RiPZ>P zF>I$cqOUAo`@Yvoa5rSGE)VP^(2hsuWu|mR+(e}D3WGGP(*fJ)EB0(i8cki;)#J_u zZOul<&FsZ44+jL8PBTks5Hp%amIh(SJpI@wNIAlf|MY8JOCQ7yK=N+j@GSDZ0-SSC9xV8e5G)ro1=~K z!C~WT3Vxz_TakbO8f2SM+3T(nVyg;7*HPs8d$Y?nto?38wXvKYPy`8)U5w;7IMG>1 zX`nbeEk&^f5+iAsTn`bcb@e3>=*^byc9xbD5~qiS z=FCVde1c1tI|U=cf9+duarO8q!^IU#gE&=U=#;KZx9{`f-7j$Wr3<>15L=+=G@Upe z@2O=W2+YR^La$tonWtBmJUZ+d#4NxMvVLrZ1kN+)jX{D_f$R3t>e`VQV7DJF-Do;- zf4pN}3N2)sMk=t|Uol=i?dHPtdan=E2Ji>I{SQ1I)A%D_{OsTR`aYkSk9TODI3D)s zlPBzl#QbnWUmi$W8Dl1eK&%0ENom}0zP@Dytkj=ihcY1;$Y;(nN@~p*U8?2~tLGAM zb^B4Ac^E*WG0|dJ*@&GW$*Y64XbflEL7{ig?dy8ZEKuBgK$&Z0UaIK?LPFAL@m)Su zxH?>z3R0Dr5-~f+>+?TU@E>C zGEJJLEsLq$L~Pz)mxbf;h)Cc9K_xMS$auMfFmjq^UcM~kR;*2lhSkr@M7_VIosOg^ zTpad%L*`;XuuGBH8@@ul0dU@uaDvhF%eQmqGcV3D##hR-=;IN zj)BdDrA(7C`=%42HIlmmX@`(}bhx$v!k1wRcDK!Xyw^rh7bUfd0Ea<%bbSH6FwYBR zS^U`vs6^7?z{Qg{Sstcu&6nd(y_SXa%JqTS0RF8%_(#7&H)UCJSlYk%v8T7>OwpVkU${OhOF>Lg^@=hyDtPk~^D9&1g%p6*dr_MdK8y@>b-NVgF}Q;YwSwKVK7j5ga4R&r)WlQh7MdOjTfa#r4x?jMqB$J>D_ycI4rJlBiuF4vhOtL-3aw>O!s4h6ThBH&YB7 z|0UFGTV2I7z5%6S05v-`anwEpBU?!jqC_+$N4zoLJTdU=^l)bz$;AcxA!DFhgF2fw z!t)uTDzSS@;PUHN{TpIT=2Bb=y{MncPW4^cnBvULb*d60k^+5cZVt4re!r)}CL_+G zgyuppgp2{5u^Ztl#>mnd)NTS(2mkCgId0(3OAL|ZbN`NwA7d1%D)-Zg`)T1=6io-l z!v*7h|KrzJ*Z=W<^jH7N|MpK{xqQXf2WA5(%L8ll#x5rgL*ja$(A=5sD`lRDF_EQH z#Ta#33r#DvR6pU_ZqVAVuS6CxCXg*fSAwl4geAtPk9vHxSF0;Y{D)uJ^NT>!RCSy0Y?f{krip4k(^NwMcP?SF^5!ifTwBC?3_-4t5sH@M?)@ZF5%xqLb zpezdy_qSHQUtSVYrq@a>!Y*f?J$?*gq$t6Xf&)U%1Em&vuf~fA0o4c+QPC}dX(L}D z8;4@Geb@DVI=AB$wucX9%|9R_B<<$eh>44g2Vj{22^;^{);)R1`u`?uU~urBVjcT+ ztv*in3Ni}E+UUobQ)$R>APnOVrg8Vv+TqkAtp45M)adZ)IAEQJtfVmi?^5JIxA zJwnq)t(B!Lv@(+f0yfUu?M8;2i8;`Ex6iq6Rw?AjD4@NQ^2pWICD&J1Mlh<4(==1- z3~ILOF=oRWTv!tTNxtQX@eraIS}p`cB8n?B{pT=oRxq&Ad;c{<5G)imc^jWq84v~$ zMhOgRx{OtASk(YtqG?HHOkA{rz~Us3s?A1zL2R?lkuHIyE2p^=#shgg{7v%kpZvKW z`l-MAtFVOrf!7CS0{}zHB?vf*-CJT9ECcQVrA(94HalpeRdMe&w$s0@<^Q$;*bNi+ zcUwgsaORBo7P0*qAux*XuCJ}?`=<&fziP|69>$v~Y_AZNxgvmXcQvY5_32(25S zCyG1u*rHq|)|=XECF!Z!jxAu;&8`;ovUV--y90q^HWgo0=)JLQbW*$dTl}DyUO=2K zthyOVnUDtGuLAx+DJCiO9c$fuj3{owr8$xFK$3u#MwtsoPXP|ePR!aXrOcFhw)d%8 z>rk5mkZmoQ&E~4qM(vY5qyjNyavG3i1{fv}Ry20*?~P(SdwiH|s(Rz(hmZkiy*QAV zE#RaU9*%dYbPoF~Ztm|$Byw`w@RS+GJrXjlRH|Cmf-x{6ooM~`TFwVkbN0pDp1QXx zt=rF7O0m|(ueD8CD*y>%y?RV!?Zj?n{um>Nl(`7jQf{DeSGps-Dp16PL-v#Vgu#L4 zjzEgUaYwwkCS6?3GVI=0Nk#(G`d$C^5BUu4dCM9l8*3jx8ZR&82VbuCV>R_qB2c>a2aY7wTQ9)mUiF zTaWds-E{Jbkn8W22F=eL(uhS1(Mdo>7 zHtWkWv&;|l+CVFpmzSo_6AMV838ghktxU^8ObY>Fp^>}NC9;c|oHHR0)M8_}Wm!wKZn!YE{qb9aS;uj%!H*#L6Buv8a3 zw=Tj`7H-RtpoLx*YY|9BN(s8fJa?^BH+uMTALMoKwueeERoC`8vf4vAJy(6szdel5 zTBR(7rB+wi**|Zb$tLQ(euq~y1Lrow9?0&j68rnLj}=j9)%fx*OOxK{W$}I{5P?UR zm#$W{QP&s}gq?wuA^3y@QV<^Q?kHv9bULvt3#aMCG*6V$SSlzdZtqWA&kK)gh9gc1J@S=(aAci zX$Gy~QLH9!Rx{a9l%~d;P!&h{ffe)%eS3dbQZg}){TJ(EplI)a`mY235Be$4LrF>x5bulLg*{c2hCuU;RR z4S>OW-Vo^_5r#mQ%5g4GW`Y`14lznEs)T3{zeF|dyIS0}3W(ame`Oi2f-vyX*pz#+ zjmkVx%G99I++`K0-TmMk8D3dp9vWkRu5LsDw5{hCQgydo<;AlqvwkQ{Yi>9Vy| zq$wdmjDuN}A#`=LUW6DDX-H<+p=NFw1jgNH6-z5DQ(<0adp!|yP8=?GJREIWaCiF> zk&d>7VSgaTOcc{E9PjVAy}9Ax?wV9+yA9fm;dn3 z{y%^F?O&C3@{f3ZU^akLJAG~+_J3Ff!Z7~1Q!5{DwG)~kO`s~JL78l1G~`T|v;l3~ z*b1}xj$#OPKXXFaFrp@#6j#J8o_n*C+(H0YdZBb>?u{m9hh+=g!$O178XTt%NnDd4 z%vdZ{UhDhb3DtE7(zg>WMst)2DVlPxn@bNaT2ch5ouyS&Iw2-Xv$6Qmw=*xkaGQD+{X`wEKzAR*E zq@974_MYLoio!CVxPK7hkT-IXc$GBd$dCgm3TUU*LZg5NV$2LWB02y{N{QN=S(J6N z`f8>7=4`|$37sItFr97+&=iC!%F;d3tap~K6u;LZN@Sr_ll_ani{CaK--7Qx1=wG42n)@F%|KfBD&8<>m4fUmqAtu%G$t z&rF~B`Okdj=imF{XTIqpUvsP4{@LF3S#RCSvEIFvw=$C$4fLvwTH9F&2BhdpIbFHq zK9)lr$8%Y3Fj}u_H+7fyff{g;Ni#~Q;6C$#ictNu@e#D~%!3D3r080FJ89Syg6k~X zcnL8kVoG+pOWGE_2BYaB(L4t_&V)1BZcLdx4y2sGU3->!G4B8Lz%oy$b_468yW05p zZWt2U8vFgo{xFiFpsmuImG)W-ZJC)*4`?eyfguNWLt;1Df;JG)vi7W&#(LyvGUDL5 z06ArL!=4!828i~)mh3i5UpWehy7YS80F*V0Q6x}{-Q&8<)W!S_@t}~_8>Kbo+L%gX zs>;84^{Tnw5tEvlh&R+e! zcUm(kEcjw{TP_4Axb$<3mH)eS)vl>B$}gdiIttfOssx(I_G`gpq0Kjd7+(Dd4pfTS ze~HOO*S^#^2i{X|X+*N6(}F@02)p+)7yue$uw1{CEEUM-^muRHnz4P2f`?YafPeevKE8pE2w&=^-)K{&iEWzu|ZN z_HTLbum9vvyk2eZKgRWeX#sxe6;k>eeTlybNxwlQImyG6YN&4IzOnq)Qy5c)y<+vc z_S$&wuTC&A6A?$=wZGyi=p&@fOYHn%%rK=oKAhAZw%VWtrN{LlHV2)Rim22r!_uY? z_Wc=$!7Qi3S^-EfyD)d61)DXi8t^H>Dwa}<&+AVt%S>;@pa#XF&}y@VDYhWNHoIZB zMZcsUMwd5Mw3(gL34FzI@5esZhf}4FmD=`dh|FPyQJ2eXnCJ{7SA5 zWCZ~5)9-xY2Y>TNzUDfI{LLxms}v(CK+2IJ59VW+jFP%~{RxnBBImdPY7QxxXWyC` zhmlx}=IY#V$LhkmnYbNx#ax0b%7RkF9}@q(b$^J#Y{IjTDq}EXO5-&(PqoEQNR#s# zaY>C1yi$#YZCv=OenFg|LNF3dDGT#-Vwz4Y(?lx^SNns%A66ZeY73;6;-^?-zdrzC zoCN|QW>W$NQ`Ci&7=}H07#W6vm0cRiHWl)Z39@xe9hYh-w;Z5BNAf~wmRGITis6osdn>mbV^0@4Lt<20UKf@ zd4ljSb$1q7!(qAszTj<5G}6;bDfcP!&BKwqha-3Q58NFem`b5)fB)OR^`HNtPk-h! z@BXSSlCR(j|L{-w>b?HwpZ~4Sch_u@?9xm8lUNY_v<+e_ zTKl;&5b^XDjQ!!yuL{&)4zbP+nmy%(#7ME$*tCL-$4 zRZLLj+>g~4pmnoHBSwgJ-}*YyVGRhy4hSCuulGg}BIfoRJP9a@a(OZGjT>Ytk>uM7JuREKN;?782+o{QrIP>1wp#`#A{Q( z`F6)kHEn%}t9EN0w;AYD!Mp;+{ysKe_~AT9mk{V`Vv(r2rO|Ytiv>U01FcZKPxM8< zdeV1i30gTT5OiAzz!~8Vku(l&SYiEg@QH@V`kgnXX|kWQ8zpwFZ~{W|&E(~>m~xd z8KBpqS7mMhBJ0=UY$3K*D!n<-DGd>GL>f{ik2{E!H$M6X!__0+e*cEkdoSo^=CB(- zu^Y#q|Ihx+pZRBicV6?-_=>I%{08tTK2^W==68Pg98Yr^FaA3b>-}1(Pzyq{xb7UG zRZ8thC}fFeR9{zP56+x!hu4fPW?>?O1cqQvqH{4Hd{d8k9QZ3Zf+2vGcH4da-?tBS zYO*~z;(i4UW2>3zjss|p=8J=MKCw0*do6A}q10uuQK7S)g7am~daPTuvMHQ!KrS<` zUWmF9qTu54nqjxM_M=p@EOUjQu?+376qMMSjrXQR57oYw;_5-6H=!(MkP)H-gE2!A z=CYX2Vob&{Sks5as8n`PuKm7E0oFF4bpv;$H-;RESmaNQ3Z+wfW2udKsZ5LY|J^Gv zvo#0N0HV~c6zwe9Ex;w2EqmxTUhYmJYpNzYSidVhKPxMUyk>*X$9t+O)A63Voam=} z?r&d&%iEW`DqlUF_XmCh0Qk#4_anFe-tYd7UzE_1cH;PO%Y!OQ?*#W@3&BeIR-0Ki zi6_5pY@~N@aMyqf5pqoCqT`#B;w-p4+DWfa zySn|isRHMv(z`H>Bg42y6{h(_9uK4s##ICW03ZNKL_t(!1{tDI=Y^6c>av)>pmyrA ztV{=AKou&)8i>i%Qi>sIF%9Ho+ommzzMb-|Wx!7J)+?=7YAZh7F;DIw3ElqeoZ7?I zR@iHrCvw;-0P)?wH>HX>{Nx0?7>FSeyMQ)&uU1j$IZ-GWjnM9knhs&ng{GGD{NlYY zF?Hd-nC_w@eBtw-rKXJNSI@Hh>e-5Zh1a+Krr&xR#`Io{>G$?-4?q;Zfz3vRBIe4n zwvw%NFAH_Mv0Hc64DBBf_A#=LnIS}?A#*083Bd$GA<#mg1)=)bu7m5+&s=~g4g@Ak z-;LfX5L_?sHnl ze9^~kPTX;j6DR#NZN34lnF{Mz&na2z#3(TkG$q?ib({aMI0>{;Xmz2M*$7MExd*~& zsob3^iwX_f05n845W}O`aukN348htyn$oN7g6gQBF+80DE@D{2$`27|GX!zRXsc+_ zII_RICJY024<|Gv`S>Tk{|K?Mldasq!pKTiF z(uC7uBcQHg^XNQm4|nV9?tKR6IyRpd@1f(?kNQkrP5-YUZV!R=&(IsF?UUvGHW?liU zE5mL_NM&C@*tHoC~3JWexd0-evVj9@(_FPaP!EX(rWe9kUO& zIhbJwVq%w7(~gfWRV|)7aNZy!tlE2P2c~7VuhGwlHQVAarQERFfvV5OE44P3Vi-kA zBjaw*IPPuXG&|X1>I`Z)z&H$~AyQlM^%TzBMjz4ETpFfi-}dhce0Ih~lM$*jt{ z6RrYlETXdk`!>dlDX^4EDPd(uIagraAPc=NCe7`Aou%5(tx9X^l58*yh2Iy}$&I@~ zi%oroVPFs?HZ{w)SDj;>htYLC-5oUDCNXEv2z55&;o^d)&)#77=n3uS0nzuUWnq~n z7{5A%fgidJ;Cny)=@am8{A=Ivn|}(4|6CgOBnqw1799nSE{lPI*va4hH1}F{?F3ky zf?7lwV+L#Gc5t^R_Ykz2VW9oTvrW~}{f84?~Sp5jk$DRGpo+jE- z$$4N%*+*MvK0~dQTCL|*HNz3pH;HX(UV?XpFTMZ%R{54v%^s|)Ir`?2^l)5>5vD8f zKOc7sV?8jsz^M0bZacVK8jKH7T@I|BUKessjAJI|$S&`YXso$@YSdC$JsM0=7tZQU zfff00ts!FW$uYTHkG_F{y;k>ET*rwKmF^mV?&ch9;Aq$L$QO(o(fmD6DKa>qyH#}9 z1VutLJi=yyMo^MhTE}jbap|@(r*Y)!>M>6qKO-J4dGB^|;h8ZpOZlf2z-ztIaB&Ax zq-n0Tprt~u7K^np09LWdYB6KGHzD{bZDJqMP7db%Hbe`5ysZn1!l}ay%wjQFT>{+! zs4n(!1)mPBhN7*jd-j>4Y?b;-bT>c`=!`KU(wI+^nM$r`6m#~>`--+^)m01* zjy6;q0j2dqjDg*d8Fra87$~So2ty`HrL~3D76VjuXT+hn@(cQxwE^}XIlqBUso@nh z;JF0vk`T<3ztm!_4OY?E<_pg7w@PF67x2xd$4C-W|Ctw=sYRQMgVqekb(Clut#JZG zZ7Uh_$T%LjI9xG~2TB)~xe$r$cL#=HMAGYx{r*a?5A6ofZ~Axa9`C-dPW4~uOvE&p zmc1@#kco9Pw^~YjWnc~#n*b*IPTx>wHGAp%4vWI9P{cNe8iW>BTWYmUU`4sL6SsCl zy~k8q5;L5PJ~8kS*4cL3X|@hL=4=w@xhzb}S>&V&%Un3kg{3sg(peVs#A_ZvI*f^_ znJaKvJTEXNQu3(=!u9p_<^sITC+2zb@u>CRF%4)CPNx&CF2pDdDcO6}s+qC|^$}o0 zL`Xwq+zkw4Hm`gKN9*F*2+}MYPzNAeLFAxf_cDCN3brKYwLHQ-9zrq}AJu8Y7985$ z=1rZUc=mv>YZZKTEEr{2bq?wkSqS!TTQeXw1i48uUqnTOMf`@19e4=3iOQ>#)cEX8~YB*39pj+2pHuC6b*y1HaY2{SPc z)Y?fYF$@DBEX%?)&6Hx&+OjMx%j^cAj*d4AVo~jsWdY2bL>4%ngqQ+( zNUm5E=*2gHXdxzXLDlTQhrDCA+fg3wjeOKVs+HH>9gkZxdGHuVZ+{f&)(Y7;)W^H5 zszTk$cjI~3P;lK5mbtNnPReEgvNFWGoVSeLJ%b<^W@Ds9yVrQ7H{_9F+#w-yJWjm0 zxn(-d91a&ee)Oo6hvUC2Y5C0ST>)R&^`YGW0QQFqXocnYz%)$+ttR9bJByuBzu9%` zdZRr?8v%)do{WuW^3u-IlpfiDzZR_QS4Wteo37lTsbALQ>dtc1f%ZW5Dc0;186;-)guxN(|~J_e4yMVN~*XKvK5%%GH=Q zbqVAC>(t-lv%nOO;5Y<{Yw_|nC z(V)O}{UZXynAtfm<6$~+cYnuR7V`e^#Sr;7e{T8lAO7CI{Q6_RzLM)hy#X92MRg#h zOL}Z*5rQN-rWbEg>7mo3(n4GBC(_kt)__2DwO;o=umuZwW`4`^dI2GQ#KhZmU@>{-zH-6)9`qzHoS9cZs^4EuY1DL1w-Mch?bbomK$xLLKBCVW^ z*@c(rvDrtkiU2jRE(RyjU^87K4LOP|;&Ibw7ov3-q~g^C>sZTw=ha2)4$dX$63wQ( z!CZ8kUx#K|gYJBIb%!HThL{PGNMiT|?gVuGBtqIlddbV18|uT+{Q)7QNFFnfpFDEh zGi>9}Fbqf@nL6B_W?mj=T33E+h{UUF!r_3#NZyUq-Nv;0`5$*Gza;lBnyO)$Y5%cct|KTV9&j0w@ANqg9>qET( z{7;|xYk&FQ{AEJs({ji$Azt_Kd|WmVsNv^U}hDiHp`1|9mNuB z0-e~;W&392sjo8D#IXh6Ao>{qtgX|7@y=`Q%oKMEiWVx-n$v#OJn~Tpk&rW~3pC>s zhy?Z*R}8xghVcNwvjI6FK{o$`)|M?5rsW|&DQkTL~tuIb_M@R!o;PI1ZTwXnbUTL+Ehs^Qej#e6Xw+~FS z8*22%ZYMn29eDceO`be?!Y~Xp?Uvpw!N-&Xc^J`ZA*-z!wxPZ;tB6~o39*5M3fM@r z_I2uWzSjF}bb!?mE?9O%H{FPB@ZGZ>S9RtZGrmGPuKE;p3p1gYg}d7uj*Gbz4LSW> z+z-EI=|8{R^`YJX0RG!gf8SpO{^ECi^S6EnG5wh^4!^_bot-GAsaJHPg=)9mK8L`t zdh+#iS`akzn0G>o`3bH}zO}sk@@xI0wI{d8s!FLsi;DlXc98|GU?KRE+0smcNLe9d}X)k*ivig;sz(-b!`vEj4wNpZ+4vBrtv|6_S&Q;6bn^h_V z!wyPwyDRNZQ?`e)ccm`HPWu>n{OB4%x!Dao+&?g#P8RCfloV_xzT6*q-rggok^AFB zd6+3v;o@RMd*e%=|045o;`5Imvpej$yT9iPfB$pLcPCPa6ap=n$SRC`q$$(G32J9L z&eVD4rdFoYJ-0W{nNIgC%ZcSQn~&f{=deHU=+P5C@{y17jX*Y2tJ|Qe5<78i7|(`qk3AR?h|WQ7Nm0*DAfZ?)k70z+`iQT3a`K;fFIpNnd$Q zfA52cNsEd7&qdp?EHW5@<@nklH`V1c0rCVsE%2`#`+saInt<1F;X@B5+ zJP8nTW(}XNZjZI736 z5}fI0D#K+dEQ{$Sblp(Tx5Ms<$|?lJ8#X#7tKtRZrw&qHeNIuap?_0!_siY9e&_`nrW>u=7E$Fhhac34(wut z-Z?!y&}&D!CF-V-*bh4n7uSXzq{MFAo8xWioaWhsL#)ks^T{(L2Ik_Toq0wOE)EBF zY2g0mj=IcDrw3DAsu6>R7$FZdIw8Qt;lSZ?PfjCa9$4mud79}c!)|21-;;;IYo=BI z(6%{#jE>&7es1QS=f4S{THlcW5YpMil@+J)Ob3-#z<<({(O?{5!w%xBdC=`@S!|z9;=FzkXe90H5Mh z^}FJ?y)-N>Gi1}-Q_WC9TSY@9Wdr9#&ki`NSZGZXUfp}ofg{4!Os*iHKfvNd5o68U zgVV(~feuUS=H)LD!AU+|+AgKot9TYbu7y-9sKu47n}DhrpR{5OzTOf+gu}RF3=!>} ztNnp#nk^u-ht0chCDw!xxVpMBqEGFN`va%biD{l4Otg~xYL({J^N3SaDa(RZi*46# zPDME;cKL!fBuW{Gz5(nHBPnMPWtmSlY8(ew`}qPxX|69YCZM?+sau1`P5JmrDZicY zVA_CgcIDOh6RRN$y51kVZz`TBMfWIhUa9QX4Wpa=Ikr)(mh4;Hs~B@LXX7QL4rA`j zp|P}@AMWp-9v<#X=Pxh-Q5V;r3zF2F zX2cYBrFBZ{o-wG@dKQ|jQC^6FU1w#RkJ|h}QzxR>W9BJBs+LT%55yGcb@4LT&C+}ap_j~#@Lu>3}WS0j*?<}Vir_v~8MvAy{&|G+G@2)H}T4;5uXH~js&b9X5r@7s0cs^7bDa3>njY32q z0j+`0SP?OXB0(KmCB{*sqOmE5RwY$xY0AVzN02C03^l>F0%Zoojxvr|&nOy80b}fj zpr%of$LsDr=X~GZYppr!k2%-=&L!CK?me$Ni}CT!J?A^$`I^1vn$53~G=ez6AYe-1 z?EmJJeEbLkOTxjBFdgin%WMtPfVQ|R5Lp8O9%NDmPX-KwHyRg#jR{>mhpD-`kT67? zR$G<|HCy!t8GyAGq#HnE8nW4(;AG>CzZR1a;c!`UN#tGQuhx(ir+^SEiWNsJ#-yf~ z7LlK!2M3bC{hK31B|ZxRLLk%$Toh%Vc~Y^)4aDbKT16fQluEE=2H<7Ob}6{}z*Sh8 zZ{CIer(BPl0C4rb`=4RjUfybfwFWVRkYLCOtboqN=l!LkYz&ngm+mz??eqgE5N2v? z7a53Bv}XWd5ZQI7jmGGZ;+MP!;OYO6yu&YW?g8NAD}vj44*<$`izOU=hk*ZP@sT03 z+jK9B#fI}yl=-0C1Aw`7If?u9X@FvSCX|N}k}{eCX~-~EaB|h3E+pYJ_{AaS- ztZAIgzS&z$N;FJWyqv0LkevNg*RGkkYu@_Twq^o-%Yw?)}8qio_ zOv@0f{qOyQ7Ya2bs~8a4fM%$r0;;PD zPHBc#!s;7cbi-w|yG2&5?UWy1r2GazgVYm>HH*5&aCaQ;+W&ng8OX&khL|5wY$Sv_ ztt2F7G#G}IP_&^g3pSe*2r*<&K;reilDpwpXa%ro_yzcrQ#34PaoH*nxW7;90%lJ$@TfSwT70&gAN(c79Xi;ajby3`6+eBCGyh9HCOR-S>|Koty;_%v#j) zyv_(s-6W=)!6fRBzw1Bt9)X$*w1ODN^NdAXa1j`bs;fYc83%L2BV|421Hdbu|6hG< z*N9CyqmB@r$w*_x)oyLQNbQX4(xb z!`y=ji&b=CkJb8HBWLWoay_$%abtr*qGSL?hq*Njc>tLcaLTfvqy;Ty6mw@HP1#RL z-Dw|6XBd)i;BDC=F@rQANdw@Z9cyk{Le0RkgU*ueghzes#()w|Y5~GgY>_iA+u^Dry}lR9`w7(PO#f7gmz81mB$uY=)VH{xDn}a^0^3;891<-=Q z26SZ|_tP^6JduK0g<^-A>jWr2!Cm>dY2e@q>t-{0$TjPRUhc>C#XbApK(7xyMyIowB%3vf$>nv!>yxqw;%RG@5b>Jb=xGy|JO z`YA{v*grS~4})*wy1>XKpK5DeaBw~G*6wp}9>FI7MjUe6G3z#UXRatC0N)f)hZ}e4 zt=1Z93KSk2S}Q2?7MqQu^oR+G0ZZ=KnKzFq0)+eok2*f3gmD}(jxLpc`e$B$w@Ce4$e-6$ z)fm{ZHE}BC-@&za89+)2S-f;|e`Ez&^UR`F6lyWySz9(fC2Bq)FmZ8LmxE6}86jYh zjB)=0F5Pk~hW&jE>4OJ{58rxB2LOg)$bt)F!i8zVo)G43L9NBxU!<6q1;^VBwxxiQ z;KIQn4sN;4ee;6$DWT7|%rhQ*@Btj3oH)7+-jGT%U>N-3Yo@T~7yat<2U_RZ7KJLq z(3*SOOCq%79p_fvoK%HCNsw4pAxd|v4KPdfhF}fNzLXr$Bf_huA&RRSNfMYfADt0_ zpn%rvMqg^5!7JN2k%C?cg5~K^~Hef|{b%621r;HsS!ZLxEe=|DOK$5uuT%>DK!(A7T%3 zjQslhAc3)vux?Y@&pCtu03ZNKL_t*ZW^7_08DVKYX=%-36igt=AW9)0uHa$B#l3yx zi z;Nophz`-rIzyJ8?=!+$k-g*FN)^K#Zec`6GuNuv@{5%NQx(RbD=;Y!K*0Gpt_ud$V zl&nIrY-bEpb|o8x=6nS-3y2)jtHq5zvIuOLP^f~4P)fmeyA8{2IfbG_NM;qNHRR?O zBvJNeU8_FIBm+_MdKO)hEUe2~gO=(WMFehjT+x;-8W}`lY7t1llroYePy%@6MSF%r zhA?Suq4EnOZ!x8DB;(Fim;lv4D!;)TK0*QhfdVpyFe?b!6m#*vS3m%{^|^7zQWUC$ zF(2UY(ruV7T>RBU^5y^gx4+>XH~-52lds2g03dT^-?>(R6f-&FTU$l3hNV{LY;(fi z;UNxhy#xDKZhKE2rf+z|PrUgpAL=omd;9JGZrVGLtK0GwsS>9BVePgvDYC?g-+*et z9aoLPv3K|xKr*;~UTa4O&~M$g)#+n_(FCkcDB%!7Q~_%-b2QHcZ2aaU5bC z8T4Dk1Oe=g!Vm}P+IfvVs31ns##pH!wlSf z>?zRbT`aq72%aT$gO$>}F;+^85=p}?xuB}69k*8Fef5A;g6p8I#pDG@q3|)6#Ycu^ ziSD}#dlVxy7N}425vJ~7U=1p3W0qXosVcz6U^ZlSdN`ZG8r(B5Pu>Tq>U@XrH$)y_ zJm9zj7zT_7``}UjCFS9VdEEQGZ~ec1_z|TB1U$wAfYtUxoB8A;3)-`*5HbVM21*1- zMkN8K3DYoO|KcSKhZlc6P5ZC=<{$i?H~wLd`ObUpdG{wi{mwUSn|+1W8PyabhPoW1 zmaWg!t*s{)MaplkAmwhu>~Fk~LCiNT#p%a!TGrCApg2*9WX&EdkR=52hL;r|H5Ekq zj_5zYrvTQ=53iBIv8mWK?K);&C_y+ONp=oDe7t7}fz8^W>c5vrQR@OVb#|oq^q{S? zHVZm)39LYj*8+{#5W<=ScP&B-Dz1~S=CYs^e|+}}q!>y1M5NoZks&fHpp}YwDOhTA zWgr5I5f%cBx)-3AEfj&)n~}S;Op;rTc@wa$lY`y{Y3f2L(?Fm!`fE&zVLZTe8IZCBX9Zfn{|yp)b*GS0B`yAU-<4%x$|kfw>N%?rQunW z{j?&XR!jL81M{54EyMD(4?uqQAO7I?y#9}ST*`8<5#Cz1^UJRtZ-`K!bqkQg$vd{n4vD5YR2ON_F+bU1wD)B*~xzTOcx zGe9DxWsXf9xVHeUXgB~ha==sL62wYkTFKkpXQX6$?b;j{e~X*#(7 zeFsHRP@sLX;7zvf>RYQzh4&Ocr4%)J9~XvIm+A_|Hel`i*A-svs>>P#ED~O8{~aRJ zvkoT)DOkK0A~1-m2Ld4aBm}VG+VKp_d)~Jp2IT?c#Vfew2_Jb?Ise%F;5A&9i(mT# zKlNk({>ER?H-0^)0{{U0tM|U=J3r_7FD=6SMf;Qc){Uyz%>9{o!0s59gvk{x0Y1k6JJMv(Nt7rEEXGE+-STy2Q09mRi8W z;5Y&;fK?#0|^QRb(Mdj2Jx`5)p>zl)Dz*1ZKgqEGVU*>(gmWDY^_; zP6-l3B10eoQ*vrC$PGI}=MV@%k{0O2UQCLyK4A+!qA=BIO#j3Fm%u004qkvX}1 zSwFdJ3B(>pM}GJ3em@O;KH`Lf3m3iAsS8>wE|XRkEM<;SBO&J;QN!jcD)<8``TES3cigFBI+DtiNN(q?&(S~g1 zw(u+<2sWDq$a~0#myj-80qG?xl6c{X<+OhTB3jQw%I-jD!YkeS?)+RP=%VXNgf1s$1^?4mV! zJzr{tYFlU0o&DAVP~%o&;v273|93RiPQ#Z${ zx7v7c`2GLjtv~kpXM5G%xb>JX0nTD=XZvhY3~eFY`Q(rMZ%ODI=Cb*fqmz3-orsVn z|4h^Nlod2XVMLh)7$=Zcpn3Dq>BCq>I>3%Er&?D&K}Qs}*48QSF%+RnpTmQP5Z+z>3 zZi|!OH$TH2l35&@?P|l$gK*+cMNz`MEVz7d2?zU!kUYG$ZS;S;=@-~dT;~Dc2CR3z z`(5|+ub=p|r+sIp`jevR_?Lg{Km8s6Jp1WS{464TCyYJ;@Y>n2o+(IZM%&i6nsOC+TXli!bVuC zGyRC%B&FB=F0Ed$)CHSbu&ALC)|1~52*9Q=)UbW`#-EP>gY1JY*b>iDfeJ^3lDbC( z%)K0zs@N1oS{nG;wQss}fB!rF?vK9h{+oGi-RyN90B-QQ`~SQ9eE{Bf`g_0pTfhIV z=RD>1tHA&O!|AL^1W3te&s@X$ilRA(o#+I2V2__jIqG<3L8ZXZ*XG^+B#^Kt@271~ z*R?r(m%t!li11n;=j;+)YN1@L4AMw}i98s@Bw!py)Fq=X#haTQ<-`n7ah0HG2)gfJ zY_L-!Nf1y=asLB~9wc^N3SDQfY#sW{oS+mI=rB}m*vt!#WRmrv1rzreO_j9TWEH1EFZc+yKjaIBA6W!K=UhhPS`-Kb_HKb<@^)0642f zcs&^|!0fvMNfx%VK<>9G`(0w47Sw2@q8q*;T( z$CtwF>3#KyfO{-p%}@e1;7Up6W}kiIF*C+t!cvN7Kw*QX2F#`6c(cXv+)$O#5SooK zwKsGil#7TY3f<@CASMMM*`d*8+b|4_S{qEFLF%Ifbhjo2gN8j^xNApkfJzLC|SO6%`36D%aTd^38A z-L#JtgOA@d$Uf{Ny%=Y9JOKjvr=3n~ee<_fN3Y}ZhdiKE4H!`@zoaYo96gQnO)M_oBMjeEFw*O z-Op|=1qMYDhKQ4dN~wTkNIagT1d#*@Ae7_*k{kxiDThEw^$vZBu6s&gFj}X}8lgKG zJ2A+?Mn;T0jHqQn)5ZDuW~(c2YsG_{`Rj53l*Y#NrK&*nTHkdP{ba6cSenDT7Y*;u zUPBmGBqAag@TRK;e7ay@az`TbsRgSdXP0@O4lct$Ki#U{W1&c-2CxhlFW>$VFT3rI zJ25u`Ut56XkwfWyDC;}`oZ%8lNa9r3Ebi4Oc8?a zOzBOx-tole{N8&%Ae1M_bb7+^=w1BB-{pMWi1lYb>Z5KU<2_G3y#bh#d*yX_E^vJV zY!#fe5WFnK<*!0Hx0?`|Ygw)1yjp=;b>+;fHq3QK)rMjf#fsnBg1OGvE?aE28ys(r zu$i}*mn~W=aE-i<80>039e&FIi{GO2jxahE$ynyc zy!ip(@BP3(|LuIs9iMV^QjS0H;8iFD^e1B+9UY-d=Q6o{nX00e=6}*U2*KBN z`h4cxf)>*R4NEra&}3$i5I6zgga&i1#g%#4i;~@AZca{M1;EgI1i&y&xODL{rfI@v z)38R=53%mM?>@A}@cZxk0EYcT4AXQ&u8W6zodKeZcTJ`#X8XK{nQl0^% zdrgf zlyn+%j2;^h%&GPL?wBbG0+bSA7zKON2qD*g%z4Dp6essTh?A27ln?#cea+{+=3|xl zC*FVUfx}W27xxaaw|{t}E{%tEod448#^hl6sTsJZxxK`W zzLPiIJss~NnP3jRd)MA47)moYsSrgPfu(NIPys4X9*~fcEJKh1QODMU!=<>ONdnAi z$E^k!r&Y%ih`NMWtE*WjQwXAJL$@n)l23Ily;4H5tA$qHDIgL2HMQ1!OlMy2Gm8g8 z3a1|HDL>X`B+wHPa$2$RE-->1WXO_{*?kTYIkcCEadhnno6Qz!I6yrWK>GU1&;1*p zIJfeTAGrU)C*ObV8ZKVG1w`~!OQ9dRsTa%{t@8kIMoXb?E~&jX_r{v_lPe^#q@TXj zPet?7Ijm?kv<0Fs7o~)E_M^-V@Jc{uD}ZF+!Rf>;(uih?CJmJvQmROjqJB@S`weq9 z1@RM{NqnYn36dO0qpikBvU3s))EZKf)v2e|g4T+&Bx5YttU;L|@YCEte)5N~XU`7u zFu-E-cjTV*I$vFogG3D-jbvK_{3LptN?A7No!(h1m_S>Dr z2dK+fxT2748gezHfj}xgdSiu3g;s-dgGj@W9TDe~vnV~nX~#%lDS$U6#VH?Z#-=ei zaJsFbK((T_f?8eON1K~^0zpV3b3K9TO~KZ5)hFm!fRvr~%!xq>u-cqEp%sY$%8a=- zNKm8u`$TYXl@PDWMy*vWZH7vP38REAz=gv@T)E{Cdk53YFI>9q_;p|P)&Kt5(aB%9 z@9NbTz5m|(asRa=D5Y&x{igZa(RbhdOLu?ZVYq~D$~q4KXS7Ux^BQRw{TdWC#5qeK z*S!9xntd90Z$ z2n8b%eI(bX3GB|ewW=fIdZCRSz@|6%S}TzO6x2BVca1;4NsJzAGc2_?J00RYhp-0rRWe?zp*e5C2L`1e^MpVrvp~tU=^7JMLvGw+>;<Wps;is#J8(ef%qFM}QH57D#At`c7 zFmWOe5yj@wAPVssRQiThW*6(EPSwTAJ2373c|Nk#5FpiHEp~1K)Md*P)Rm9}@1W`g zCuRz+g1Cu+w$K)|C~=%n)zr}D;`b+iPeOw!@B{{{4y*#)m3SHlz?!@FMy7S`SOVxYXNUXto`L)M#bb$M3sO$V zDI;ZXn)Y5tCjt?(f8QEvHSiD;?F4Xg7bk)@4DHl=m4%U0Lap9;0SWRjxsz`bEX}hC z88Qy{4{_<%TR(7k;o_TL{{wIQ(zCx{&SsqlfU{cdl)ip#Zgnl#dsWKE_slCfG zQc76V+$%8xRUh-&+TiTW#hqy1`m$D#jzbU-Cw}Gx;{=lIVkAkBl20=nLJVgEw&sQ; zET9As0*oa7J1(uJu-ag?M!+x(+4TvQWpO8{=8|EV20Y=mJ8}CRPr`%8cE9q}@_2yh z-~f3XzvmzPAK&(sXLN-;EbBY~oY7(-FefB-9lBZ?iYUfWASr_pW`ybl8-O5`z==aY zAD;UL?Ue{&e}$DeloNr|u%5t6(e*yWz?gxmfOR69>ROsuOc#zfv#bC1KZhOufEh@M zT__|nfIjZ?n!5$z$8`YBG%U&ktxGG5WpnhNuy5ZzCJrGvf#WDA*3eb9(??7`a2H!WSu#f#HYVCf%_3x7#Xn=Ov z3ekU#?8`YpwKEkh080He`R~oOgs^DPp##Cx;m@jpG9p(7s@0quV1P)*m?zM9h&hyr z9GIw(!5e;Ka=20w4giujRP(e4YQRZhY%B1<(R}}d$Jbu+(Pj9J|Ml*>3+}%A=3Wqw zpmiPq&R_}AQ(@R&<-0<;hiJF44WY6d`gvw<2kNyqt#_ghbB#@_3>UjFS<`W0hHyp} zLCGcj3>#{2@r`$-fWhnVt`(Sac0?g!s(|3otA%oqpsBmtcFC_`-^M22Wd_Uv5615M zd}@OaL%0`9LZ>tX!6||Vn3S_OKaHa7aTAU)51{D)G#x^auw5$ZJfpP*$rwon3%~K(|LL!y&nO>9>pTFQ!D143(rFbc3hK7q z6k|7QPz2beH{Oh3$}nx-qyu7OMqM164d%@*9&-5QHLt0&H}{5?G0!vRvS8rk0-x%m zyjq>^3wV&}YQ8%aA^7tE)*Aq%lmTcU5AD>34{{TCjv1|~MM?uy z2}he5ixPO;d*`&j_qG4eJAVCNpWW5)$Xe$C;0)F{X3UKN4Y!@BZ5b~0<@?wrx+O_; z=6&{G7a$F^@&SZsrNs78Tiazmj<#p|Ka{DIyL5ZXgI43<+`)BQYGs2SU+sFBX6m7^VYUxOn+}OXGj~ zhByD{H=ce;&-pK&`WwcHnv5^3Y$)W~ztg2#F-?2eKfH{Tr(ZDP*S`HHf9g#)_mh9* zuk!$K2FszTge(a|CZuFwE>7)D2`Pp1ue$oqyq!^`VaO9CC6wYdHz;hw2w)MUltceN zfkSNC9V3*ZsF z&I7<1tgil>xFID%Vg<9G=qZU`*eL<#($m{z22jD2Fbo4)OJMOXJ8?A2*gz8XH*?T& zu}Xn!_=2(cn9&1G_HKA}&`(Y-7@AT>)rM3W4h|D)YuIjPq;!ILDLw)eC+)xtIgc={ zP}_-)^u}aQ_q~h=2y_kNP8Sp9$P8R{Gs=dYxJV~KImjt!!;$`5I2x1u;g{~)-}NhR z1@PAEzWHnKxfvhrqkf$SfHPRrG@)RD*zV*cBBWE|mp#32$%Q$(TMhsW!vJGKYrPzZ zlO8d@M?-4{g~GHq(t>iF6LK0{*1O}7X=e@Anu5rJTfms6J%9w$)NpWk(cODPU8t0T zQfD{xG=jG15kyl{q^tS*V*w8Nb;AA*2hAN6K(1=MQx*!f;+Q@5*Fj*Y`V^ovMOHgK z89iSQVVwtnGgv8Sw1uEWT}E4+;_Kb?7LKp+rrNM)@)}WELOZU$U*(_@f7Mp0ACbZVwd=!9-VtReezR!`65r!dfql;B{ zFXr{?s!8a+DlFF9f+uF1a*GC`Y6<4hi&BwAjXl(;gf)XL=(j*^xW3+My&Gya0Y8B9smS{EP^Di zTw_7t?+Ca)g6oaJrmlhS9)Yr^^~AQyVu7ot%VuumX@(lsTVOXEF#y(F5G17>BRP)L zJx*m#0rbf*BiG_@1|!GJJLW(jl5pwLWwd3%vdp0n6r`l)`u#3ST2?k;HzM&2VHW^e z$e>wh{q4$x7(kLYP&K>)JJ`@b>xlAvJ+yTm0M25m{%vdJ>vNKi8?qoL1K9#?4J=rD z0bMDlE9+7-G^;Q@r4)<+<&e6(cR*(QrnWB35sEmDIAmx5cWU*Fg5t#Q4nIUb_h-(# z5o1_t5oez?u>0=~vX7d!NNMx{pbch@E2zr?24Qn@0yW3^BLWEZ+CO9nQdd6akoAsR zSN`!RVUS8tc4{-LL|@=M0NjXmUPzt6`pI|v{F^RJ<9iSG_K<^;M}fijbeuy8IDu1w z5>QOtqb{Ye4s&-OHMnCCC~EcW_gd`FiNlAN{2yFfBmf-ibNnu{`aUlpz$NAmRSwxct~C;-k5;EK#-ZV7qJvQuBrzI5t_1 z#-AHwtHw!6E*D;^2#g}+=z7^0M2M75$N1NPz)(y$b5=T7p%Ec zEFa>23Ja!TK&=&Ih`GH_4Qi_{izNvuB}djN1!B)uNG*Y!{L>Uf4XW-NXykZ+0PDfb z{RO!r7say=$Kd-kgNd;ew=Hj_dB?uBRUA~SYXLZg*}&o+eO>&7L->OP$m0<3;Hdpq zRiQvZ_Mm~zIfy4oU0vmTJ=Ap`0M2NYc?&Va-rgQA>@zYgD9Z->EqMGXl3Xg!G`ZznU_Zuwg zd%vGIFEi%jV>j}&il!CXN=ScIm^E||k5i0kP4hVnNu5gUlY=e}vf755KO_T`bcPcDe$C)EWrRPTboYU7p)?HT6V1hK~#p z@ZYZU=U|`Ks(TBnI?6D+YOrJnBY6{%*HgU(!w-N@EKX$tviblptSr9i0|M<5zjofe zxR~-_9v1behpz2OFm-;3G-~xrs4_@VUQ@c{H#^ddng<1<*ZD_hg zjozT@Sb%=>$1oT85Fbs72y!$y5pZQ{4LJ{y0d#kSH6+E&fF`hh`U|^d7`Zy|`FiN< zJOJFp_39VD_{x~`+m4T}K8vm79IQGR++lYGwuJgF1V#xg+0`x8K-z$i*m9R1KiRt^ zml@pbB9`P47x|~?+tVlFyNEI2eYZ>@= zq2~eMhOF}daFf=Tyy6v4gW}uozvsQ5aCG&4851xbj@TOqjM-1%r4;1UIr@H)J18fE zB!{470g0}%T+9L_pQSs!mwZ&Hk@AOX4-o-1B*H62-R+GwjmbctC;$Xv=NO1#oyDuM zD-&yT$DX>N)q3Z+Uyr{BcNqa76-;~ zzpx#O>lRtbvj9q7>%#Q`mr_A+Z@Vny8>Lk+3&v?LYi^L+iO)_DN932QmpJeAt=%H`^Ppf+Pn zZs`pzP;ZihIS0iS4)o!4>f~os4cA?GD&Cqm1bZXR=jE=XLjj7;`&q z
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 Z`A-dW=tY;mVRrxk002ovPDHLkV1m76`(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 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 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 73f4a7ef..b34f318c 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 42ea77c0..73d2ee1f 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 19236e1b..cd8aa246 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 c9093e77..d66ec184 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 806fa106..2b29acaa 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 cb266c6e..2cb60f0e 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 fbd8ee9b..c0729227 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 f2ec20f7..cf0e5784 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 eab55c9a..a5cc3a39 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 8643f0f0..0fca02a7 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 907672f2..de185e5f 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 308503e3..62c08b2a 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 00000000..f5c59363 --- /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 00000000..191b4184 --- /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 00000000..8306af84 --- /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 00000000..8b149c7d --- /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 00000000..519d59aa --- /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 00000000..73a85d76 --- /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 00000000..94726c6b --- /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 00000000..b618ab71 --- /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 00000000..7b7da86c --- /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 00000000..a1de4d9b --- /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 00000000..e9d79327 --- /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 00000000..8ea35be6 --- /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 00000000..11192cbe --- /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 00000000..450d6ccc --- /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 00000000..363eebfb --- /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 00000000..92759804 --- /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 00000000..b54c592f --- /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 00000000..2affa17f --- /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 00000000..727ec89d --- /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 00000000..91ed6af0 --- /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 67a7a45e..409462a2 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 00000000..74da222a --- /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 00000000..3a12e04e --- /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 00000000..076c806b --- /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 00000000..7cb21df7 --- /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"