feat(RD): add texture pack downloader
This extends the resource pack downloader, with the custom behavior of filtering the versions that shows up, to those <= 1.6. As always, Flame is funky and requires a bit more workarounds than average. This will also get a nice improvement when the Version parsing and comparison PR gets merged! :D Signed-off-by: flow <flowlnlnln@gmail.com>
This commit is contained in:
parent
b724607e31
commit
ada5e88eb9
@ -611,6 +611,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
|
|||||||
|
|
||||||
m_settings->registerSetting("ModDownloadGeometry", "");
|
m_settings->registerSetting("ModDownloadGeometry", "");
|
||||||
m_settings->registerSetting("RPDownloadGeometry", "");
|
m_settings->registerSetting("RPDownloadGeometry", "");
|
||||||
|
m_settings->registerSetting("TPDownloadGeometry", "");
|
||||||
m_settings->registerSetting("ShaderDownloadGeometry", "");
|
m_settings->registerSetting("ShaderDownloadGeometry", "");
|
||||||
|
|
||||||
// HACK: This code feels so stupid is there a less stupid way of doing this?
|
// HACK: This code feels so stupid is there a less stupid way of doing this?
|
||||||
|
@ -720,6 +720,7 @@ SET(LAUNCHER_SOURCES
|
|||||||
ui/pages/instance/ManagedPackPage.cpp
|
ui/pages/instance/ManagedPackPage.cpp
|
||||||
ui/pages/instance/ManagedPackPage.h
|
ui/pages/instance/ManagedPackPage.h
|
||||||
ui/pages/instance/TexturePackPage.h
|
ui/pages/instance/TexturePackPage.h
|
||||||
|
ui/pages/instance/TexturePackPage.cpp
|
||||||
ui/pages/instance/ResourcePackPage.h
|
ui/pages/instance/ResourcePackPage.h
|
||||||
ui/pages/instance/ResourcePackPage.cpp
|
ui/pages/instance/ResourcePackPage.cpp
|
||||||
ui/pages/instance/ShaderPackPage.h
|
ui/pages/instance/ShaderPackPage.h
|
||||||
@ -778,6 +779,10 @@ SET(LAUNCHER_SOURCES
|
|||||||
ui/pages/modplatform/ResourcePackPage.cpp
|
ui/pages/modplatform/ResourcePackPage.cpp
|
||||||
ui/pages/modplatform/ResourcePackModel.cpp
|
ui/pages/modplatform/ResourcePackModel.cpp
|
||||||
|
|
||||||
|
# Needed for MOC to find them without a corresponding .cpp
|
||||||
|
ui/pages/modplatform/TexturePackPage.h
|
||||||
|
ui/pages/modplatform/TexturePackModel.cpp
|
||||||
|
|
||||||
ui/pages/modplatform/ShaderPackPage.cpp
|
ui/pages/modplatform/ShaderPackPage.cpp
|
||||||
ui/pages/modplatform/ShaderPackModel.cpp
|
ui/pages/modplatform/ShaderPackModel.cpp
|
||||||
|
|
||||||
|
@ -99,6 +99,11 @@ QString Meta::Version::localFilename() const
|
|||||||
return m_uid + '/' + m_version + ".json";
|
return m_uid + '/' + m_version + ".json";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::Version Meta::Version::toComparableVersion() const
|
||||||
|
{
|
||||||
|
return { const_cast<Meta::Version*>(this)->descriptor() };
|
||||||
|
}
|
||||||
|
|
||||||
void Meta::Version::setType(const QString &type)
|
void Meta::Version::setType(const QString &type)
|
||||||
{
|
{
|
||||||
m_type = type;
|
m_type = type;
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "BaseVersion.h"
|
#include "BaseVersion.h"
|
||||||
|
#include "../Version.h"
|
||||||
|
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
@ -85,6 +86,8 @@ public:
|
|||||||
|
|
||||||
QString localFilename() const override;
|
QString localFilename() const override;
|
||||||
|
|
||||||
|
[[nodiscard]] ::Version toComparableVersion() const;
|
||||||
|
|
||||||
public: // for usage by format parsers only
|
public: // for usage by format parsers only
|
||||||
void setType(const QString &type);
|
void setType(const QString &type);
|
||||||
void setTime(const qint64 time);
|
void setTime(const qint64 time);
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
#include "minecraft/mod/ModFolderModel.h"
|
#include "minecraft/mod/ModFolderModel.h"
|
||||||
#include "minecraft/mod/ResourcePackFolderModel.h"
|
#include "minecraft/mod/ResourcePackFolderModel.h"
|
||||||
|
#include "minecraft/mod/TexturePackFolderModel.h"
|
||||||
#include "minecraft/mod/ShaderPackFolderModel.h"
|
#include "minecraft/mod/ShaderPackFolderModel.h"
|
||||||
|
|
||||||
#include "ui/dialogs/ReviewMessageBox.h"
|
#include "ui/dialogs/ReviewMessageBox.h"
|
||||||
@ -258,6 +259,32 @@ QList<BasePage*> ResourcePackDownloadDialog::getPages()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TexturePackDownloadDialog::TexturePackDownloadDialog(QWidget* parent,
|
||||||
|
const std::shared_ptr<TexturePackFolderModel>& resource_packs,
|
||||||
|
BaseInstance* instance)
|
||||||
|
: ResourceDownloadDialog(parent, resource_packs), m_instance(instance)
|
||||||
|
{
|
||||||
|
setWindowTitle(dialogTitle());
|
||||||
|
|
||||||
|
initializeContainer();
|
||||||
|
connectButtons();
|
||||||
|
|
||||||
|
if (!geometrySaveKey().isEmpty())
|
||||||
|
restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<BasePage*> TexturePackDownloadDialog::getPages()
|
||||||
|
{
|
||||||
|
QList<BasePage*> pages;
|
||||||
|
|
||||||
|
pages.append(ModrinthTexturePackPage::create(this, *m_instance));
|
||||||
|
if (APPLICATION->capabilities() & Application::SupportsFlame)
|
||||||
|
pages.append(FlameTexturePackPage::create(this, *m_instance));
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ShaderPackDownloadDialog::ShaderPackDownloadDialog(QWidget* parent,
|
ShaderPackDownloadDialog::ShaderPackDownloadDialog(QWidget* parent,
|
||||||
const std::shared_ptr<ShaderPackFolderModel>& shaders,
|
const std::shared_ptr<ShaderPackFolderModel>& shaders,
|
||||||
BaseInstance* instance)
|
BaseInstance* instance)
|
||||||
|
@ -36,6 +36,7 @@ class QDialogButtonBox;
|
|||||||
class ResourceDownloadTask;
|
class ResourceDownloadTask;
|
||||||
class ResourceFolderModel;
|
class ResourceFolderModel;
|
||||||
class ResourcePackFolderModel;
|
class ResourcePackFolderModel;
|
||||||
|
class TexturePackFolderModel;
|
||||||
class ShaderPackFolderModel;
|
class ShaderPackFolderModel;
|
||||||
|
|
||||||
namespace ResourceDownload {
|
namespace ResourceDownload {
|
||||||
@ -129,6 +130,25 @@ class ResourcePackDownloadDialog final : public ResourceDownloadDialog {
|
|||||||
BaseInstance* m_instance;
|
BaseInstance* m_instance;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class TexturePackDownloadDialog final : public ResourceDownloadDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TexturePackDownloadDialog(QWidget* parent,
|
||||||
|
const std::shared_ptr<TexturePackFolderModel>& resource_packs,
|
||||||
|
BaseInstance* instance);
|
||||||
|
~TexturePackDownloadDialog() override = default;
|
||||||
|
|
||||||
|
//: String that gets appended to the texture pack download dialog title ("Download " + resourcesString())
|
||||||
|
[[nodiscard]] QString resourcesString() const override { return tr("texture packs"); }
|
||||||
|
[[nodiscard]] QString geometrySaveKey() const override { return "TPDownloadGeometry"; }
|
||||||
|
|
||||||
|
QList<BasePage*> getPages() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
BaseInstance* m_instance;
|
||||||
|
};
|
||||||
|
|
||||||
class ShaderPackDownloadDialog final : public ResourceDownloadDialog {
|
class ShaderPackDownloadDialog final : public ResourceDownloadDialog {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
|
104
launcher/ui/pages/instance/TexturePackPage.cpp
Normal file
104
launcher/ui/pages/instance/TexturePackPage.cpp
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* PolyMC - Minecraft Launcher
|
||||||
|
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* This file incorporates work covered by the following copyright and
|
||||||
|
* permission notice:
|
||||||
|
*
|
||||||
|
* Copyright 2013-2021 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 "TexturePackPage.h"
|
||||||
|
|
||||||
|
#include "ResourceDownloadTask.h"
|
||||||
|
|
||||||
|
#include "minecraft/mod/TexturePack.h"
|
||||||
|
|
||||||
|
#include "ui/dialogs/CustomMessageBox.h"
|
||||||
|
#include "ui/dialogs/ProgressDialog.h"
|
||||||
|
#include "ui/dialogs/ResourceDownloadDialog.h"
|
||||||
|
|
||||||
|
TexturePackPage::TexturePackPage(MinecraftInstance* instance, std::shared_ptr<TexturePackFolderModel> model, QWidget* parent)
|
||||||
|
: ExternalResourcesPage(instance, model, parent)
|
||||||
|
{
|
||||||
|
ui->actionDownloadItem->setText(tr("Download TPs"));
|
||||||
|
ui->actionDownloadItem->setToolTip(tr("Download TPs from online platforms"));
|
||||||
|
ui->actionDownloadItem->setEnabled(true);
|
||||||
|
connect(ui->actionDownloadItem, &QAction::triggered, this, &TexturePackPage::downloadTPs);
|
||||||
|
ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem);
|
||||||
|
|
||||||
|
ui->actionViewConfigs->setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TexturePackPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous)
|
||||||
|
{
|
||||||
|
auto sourceCurrent = m_filterModel->mapToSource(current);
|
||||||
|
int row = sourceCurrent.row();
|
||||||
|
auto& rp = static_cast<TexturePack&>(m_model->at(row));
|
||||||
|
ui->frame->updateWithTexturePack(rp);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TexturePackPage::downloadTPs()
|
||||||
|
{
|
||||||
|
if (!m_controlsEnabled)
|
||||||
|
return;
|
||||||
|
if (m_instance->typeName() != "Minecraft")
|
||||||
|
return; // this is a null instance or a legacy instance
|
||||||
|
|
||||||
|
ResourceDownload::TexturePackDownloadDialog mdownload(this, std::static_pointer_cast<TexturePackFolderModel>(m_model), m_instance);
|
||||||
|
if (mdownload.exec()) {
|
||||||
|
auto tasks = new ConcurrentTask(this);
|
||||||
|
connect(tasks, &Task::failed, [this, tasks](QString reason) {
|
||||||
|
CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show();
|
||||||
|
tasks->deleteLater();
|
||||||
|
});
|
||||||
|
connect(tasks, &Task::aborted, [this, tasks]() {
|
||||||
|
CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show();
|
||||||
|
tasks->deleteLater();
|
||||||
|
});
|
||||||
|
connect(tasks, &Task::succeeded, [this, tasks]() {
|
||||||
|
QStringList warnings = tasks->warnings();
|
||||||
|
if (warnings.count())
|
||||||
|
CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
|
||||||
|
|
||||||
|
tasks->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
for (auto& task : mdownload.getTasks()) {
|
||||||
|
tasks->addTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgressDialog loadDialog(this);
|
||||||
|
loadDialog.setSkipButton(true, tr("Abort"));
|
||||||
|
loadDialog.execWithTask(tasks);
|
||||||
|
|
||||||
|
m_model->update();
|
||||||
|
}
|
||||||
|
}
|
@ -39,18 +39,12 @@
|
|||||||
#include "ui_ExternalResourcesPage.h"
|
#include "ui_ExternalResourcesPage.h"
|
||||||
|
|
||||||
#include "minecraft/mod/TexturePackFolderModel.h"
|
#include "minecraft/mod/TexturePackFolderModel.h"
|
||||||
#include "minecraft/mod/TexturePack.h"
|
|
||||||
|
|
||||||
class TexturePackPage : public ExternalResourcesPage
|
class TexturePackPage : public ExternalResourcesPage
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit TexturePackPage(MinecraftInstance *instance, std::shared_ptr<TexturePackFolderModel> model, QWidget *parent = 0)
|
explicit TexturePackPage(MinecraftInstance *instance, std::shared_ptr<TexturePackFolderModel> model, QWidget* parent = nullptr);
|
||||||
: ExternalResourcesPage(instance, model, parent)
|
|
||||||
{
|
|
||||||
ui->actionViewConfigs->setVisible(false);
|
|
||||||
}
|
|
||||||
virtual ~TexturePackPage() {}
|
|
||||||
|
|
||||||
QString displayName() const override { return tr("Texture packs"); }
|
QString displayName() const override { return tr("Texture packs"); }
|
||||||
QIcon icon() const override { return APPLICATION->getThemedIcon("resourcepacks"); }
|
QIcon icon() const override { return APPLICATION->getThemedIcon("resourcepacks"); }
|
||||||
@ -63,13 +57,6 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override
|
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override;
|
||||||
{
|
void downloadTPs();
|
||||||
auto sourceCurrent = m_filterModel->mapToSource(current);
|
|
||||||
int row = sourceCurrent.row();
|
|
||||||
auto& rp = static_cast<TexturePack&>(m_model->at(row));
|
|
||||||
ui->frame->updateWithTexturePack(rp);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
@ -98,7 +98,7 @@ class ResourceModel : public QAbstractListModel {
|
|||||||
|
|
||||||
/** Functions to load data into a pack.
|
/** Functions to load data into a pack.
|
||||||
*
|
*
|
||||||
* Those are needed for the same reason as ddocumentToArray, and NEED to be re-implemented in the same way.
|
* Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&);
|
virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&);
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
namespace ResourceDownload {
|
namespace ResourceDownload {
|
||||||
|
|
||||||
ResourcePackResourcePage::ResourcePackResourcePage(ResourcePackDownloadDialog* dialog, BaseInstance& instance)
|
ResourcePackResourcePage::ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance)
|
||||||
: ResourcePage(dialog, instance)
|
: ResourcePage(dialog, instance)
|
||||||
{
|
{
|
||||||
connect(m_ui->searchButton, &QPushButton::clicked, this, &ResourcePackResourcePage::triggerSearch);
|
connect(m_ui->searchButton, &QPushButton::clicked, this, &ResourcePackResourcePage::triggerSearch);
|
||||||
|
@ -39,7 +39,7 @@ class ResourcePackResourcePage : public ResourcePage {
|
|||||||
[[nodiscard]] QMap<QString, QString> urlHandlers() const override;
|
[[nodiscard]] QMap<QString, QString> urlHandlers() const override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
ResourcePackResourcePage(ResourcePackDownloadDialog* dialog, BaseInstance& instance);
|
ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance);
|
||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
void triggerSearch() override;
|
void triggerSearch() override;
|
||||||
|
80
launcher/ui/pages/modplatform/TexturePackModel.cpp
Normal file
80
launcher/ui/pages/modplatform/TexturePackModel.cpp
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
#include "TexturePackModel.h"
|
||||||
|
|
||||||
|
#include "Application.h"
|
||||||
|
|
||||||
|
#include "meta/Index.h"
|
||||||
|
#include "meta/Version.h"
|
||||||
|
|
||||||
|
static std::list<Version> s_availableVersions = {};
|
||||||
|
|
||||||
|
namespace ResourceDownload {
|
||||||
|
TexturePackResourceModel::TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api)
|
||||||
|
: ResourcePackResourceModel(inst, api), m_version_list(APPLICATION->metadataIndex()->get("net.minecraft"))
|
||||||
|
{
|
||||||
|
if (!m_version_list->isLoaded()) {
|
||||||
|
qDebug() << "Loading version list...";
|
||||||
|
auto task = m_version_list->getLoadTask();
|
||||||
|
if (!task->isRunning())
|
||||||
|
task->start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void waitOnVersionListLoad(Meta::VersionList::Ptr version_list)
|
||||||
|
{
|
||||||
|
QEventLoop load_version_list_loop;
|
||||||
|
|
||||||
|
QTimer time_limit_for_list_load;
|
||||||
|
time_limit_for_list_load.setTimerType(Qt::TimerType::CoarseTimer);
|
||||||
|
time_limit_for_list_load.setSingleShot(true);
|
||||||
|
time_limit_for_list_load.callOnTimeout(&load_version_list_loop, &QEventLoop::quit);
|
||||||
|
time_limit_for_list_load.start(4000);
|
||||||
|
|
||||||
|
auto task = version_list->getLoadTask();
|
||||||
|
QObject::connect(task.get(), &Task::finished, &load_version_list_loop, &QEventLoop::quit);
|
||||||
|
|
||||||
|
load_version_list_loop.exec();
|
||||||
|
if (time_limit_for_list_load.isActive())
|
||||||
|
time_limit_for_list_load.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceAPI::SearchArgs TexturePackResourceModel::createSearchArguments()
|
||||||
|
{
|
||||||
|
if (s_availableVersions.empty())
|
||||||
|
waitOnVersionListLoad(m_version_list);
|
||||||
|
|
||||||
|
auto args = ResourcePackResourceModel::createSearchArguments();
|
||||||
|
|
||||||
|
if (!m_version_list->isLoaded()) {
|
||||||
|
qCritical() << "The version list could not be loaded. Falling back to showing all entries.";
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_availableVersions.empty()) {
|
||||||
|
for (auto&& version : m_version_list->versions()) {
|
||||||
|
// FIXME: This duplicates the logic in meta for the 'texturepacks' trait. However, we don't have access to that
|
||||||
|
// information from the index file alone. Also, downloading every version's file isn't a very good idea.
|
||||||
|
if (auto ver = version->toComparableVersion(); ver <= maximumTexturePackVersion())
|
||||||
|
s_availableVersions.push_back(ver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_ASSERT(!s_availableVersions.empty());
|
||||||
|
|
||||||
|
args.versions = s_availableVersions;
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceAPI::VersionSearchArgs TexturePackResourceModel::createVersionsArguments(QModelIndex& entry)
|
||||||
|
{
|
||||||
|
auto args = ResourcePackResourceModel::createVersionsArguments(entry);
|
||||||
|
if (!m_version_list->isLoaded()) {
|
||||||
|
qCritical() << "The version list could not be loaded. Falling back to showing all entries.";
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.mcVersions = s_availableVersions;
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ResourceDownload
|
23
launcher/ui/pages/modplatform/TexturePackModel.h
Normal file
23
launcher/ui/pages/modplatform/TexturePackModel.h
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "meta/VersionList.h"
|
||||||
|
#include "ui/pages/modplatform/ResourcePackModel.h"
|
||||||
|
|
||||||
|
namespace ResourceDownload {
|
||||||
|
|
||||||
|
class TexturePackResourceModel : public ResourcePackResourceModel {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api);
|
||||||
|
|
||||||
|
[[nodiscard]] inline ::Version maximumTexturePackVersion() const { return { "1.6" }; }
|
||||||
|
|
||||||
|
ResourceAPI::SearchArgs createSearchArguments() override;
|
||||||
|
ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Meta::VersionList::Ptr m_version_list;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ResourceDownload
|
46
launcher/ui/pages/modplatform/TexturePackPage.h
Normal file
46
launcher/ui/pages/modplatform/TexturePackPage.h
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ui_ResourcePage.h"
|
||||||
|
#include "ui/dialogs/ResourceDownloadDialog.h"
|
||||||
|
#include "ui/pages/modplatform/ResourcePackPage.h"
|
||||||
|
#include "ui/pages/modplatform/TexturePackModel.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class ResourcePage;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ResourceDownload {
|
||||||
|
|
||||||
|
class TexturePackDownloadDialog;
|
||||||
|
|
||||||
|
class TexturePackResourcePage : public ResourcePackResourcePage {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
template <typename T>
|
||||||
|
static T* create(TexturePackDownloadDialog* dialog, BaseInstance& instance)
|
||||||
|
{
|
||||||
|
auto page = new T(dialog, instance);
|
||||||
|
auto model = static_cast<TexturePackResourceModel*>(page->getModel());
|
||||||
|
|
||||||
|
connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList);
|
||||||
|
connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi);
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
//: The plural version of 'texture pack'
|
||||||
|
[[nodiscard]] inline QString resourcesString() const override { return tr("texture packs"); }
|
||||||
|
//: The singular version of 'texture packs'
|
||||||
|
[[nodiscard]] inline QString resourceString() const override { return tr("texture pack"); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
TexturePackResourcePage(TexturePackDownloadDialog* dialog, BaseInstance& instance)
|
||||||
|
: ResourcePackResourcePage(dialog, instance)
|
||||||
|
{
|
||||||
|
connect(m_ui->searchButton, &QPushButton::clicked, this, &TexturePackResourcePage::triggerSearch);
|
||||||
|
connect(m_ui->packView, &QListView::doubleClicked, this, &TexturePackResourcePage::onResourceSelected);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ResourceDownload
|
@ -57,5 +57,63 @@ auto FlameResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonA
|
|||||||
return Json::ensureArray(obj.object(), "data");
|
return Json::ensureArray(obj.object(), "data");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FlameTexturePackModel::FlameTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new FlameAPI) {}
|
||||||
|
|
||||||
|
void FlameTexturePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
|
||||||
|
{
|
||||||
|
FlameMod::loadIndexedPack(m, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We already deal with the URLs when initializing the pack, due to the API response's structure
|
||||||
|
void FlameTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
|
||||||
|
{
|
||||||
|
FlameMod::loadBody(m, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlameTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
|
||||||
|
{
|
||||||
|
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
|
||||||
|
|
||||||
|
QVector<ModPlatform::IndexedVersion> filtered_versions(m.versions.size());
|
||||||
|
|
||||||
|
// FIXME: Client-side version filtering. This won't take into account any user-selected filtering.
|
||||||
|
for (auto const& version : m.versions) {
|
||||||
|
auto const& mc_versions = version.mcVersion;
|
||||||
|
|
||||||
|
if (std::any_of(mc_versions.constBegin(), mc_versions.constEnd(),
|
||||||
|
[this](auto const& mc_version){ return Version(mc_version) <= maximumTexturePackVersion(); }))
|
||||||
|
filtered_versions.push_back(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
m.versions = filtered_versions;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceAPI::SearchArgs FlameTexturePackModel::createSearchArguments()
|
||||||
|
{
|
||||||
|
auto args = TexturePackResourceModel::createSearchArguments();
|
||||||
|
|
||||||
|
auto profile = static_cast<const MinecraftInstance&>(m_base_instance).getPackProfile();
|
||||||
|
QString instance_minecraft_version = profile->getComponentVersion("net.minecraft");
|
||||||
|
|
||||||
|
// Bypass the texture pack logic, because we can't do multiple versions in the API query
|
||||||
|
args.versions = { instance_minecraft_version };
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceAPI::VersionSearchArgs FlameTexturePackModel::createVersionsArguments(QModelIndex& entry)
|
||||||
|
{
|
||||||
|
auto args = TexturePackResourceModel::createVersionsArguments(entry);
|
||||||
|
|
||||||
|
// Bypass the texture pack logic, because we can't do multiple versions in the API query
|
||||||
|
args.mcVersions = {};
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto FlameTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
|
||||||
|
{
|
||||||
|
return Json::ensureArray(obj.object(), "data");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace ResourceDownload
|
} // namespace ResourceDownload
|
||||||
|
@ -46,4 +46,25 @@ class FlameResourcePackModel : public ResourcePackResourceModel {
|
|||||||
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
|
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class FlameTexturePackModel : public TexturePackResourceModel {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
FlameTexturePackModel(const BaseInstance&);
|
||||||
|
~FlameTexturePackModel() override = default;
|
||||||
|
|
||||||
|
private:
|
||||||
|
[[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; }
|
||||||
|
[[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); }
|
||||||
|
|
||||||
|
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
|
||||||
|
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
|
||||||
|
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
|
||||||
|
|
||||||
|
ResourceAPI::SearchArgs createSearchArguments() override;
|
||||||
|
ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override;
|
||||||
|
|
||||||
|
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace ResourceDownload
|
} // namespace ResourceDownload
|
||||||
|
@ -133,10 +133,50 @@ void FlameResourcePackPage::openUrl(const QUrl& url)
|
|||||||
ResourcePackResourcePage::openUrl(url);
|
ResourcePackResourcePage::openUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FlameTexturePackPage::FlameTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance)
|
||||||
|
: TexturePackResourcePage(dialog, instance)
|
||||||
|
{
|
||||||
|
m_model = new FlameTexturePackModel(instance);
|
||||||
|
m_ui->packView->setModel(m_model);
|
||||||
|
|
||||||
|
addSortings();
|
||||||
|
|
||||||
|
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
|
||||||
|
// so it's best not to connect them in the parent's contructor...
|
||||||
|
connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
|
||||||
|
connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameTexturePackPage::onSelectionChanged);
|
||||||
|
connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameTexturePackPage::onVersionSelectionChanged);
|
||||||
|
connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameTexturePackPage::onResourceSelected);
|
||||||
|
|
||||||
|
m_ui->packDescription->setMetaEntry(metaEntryBase());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FlameTexturePackPage::optedOut(ModPlatform::IndexedVersion& ver) const
|
||||||
|
{
|
||||||
|
return isOptedOut(ver);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlameTexturePackPage::openUrl(const QUrl& url)
|
||||||
|
{
|
||||||
|
if (url.scheme().isEmpty()) {
|
||||||
|
QString query = url.query(QUrl::FullyDecoded);
|
||||||
|
|
||||||
|
if (query.startsWith("remoteUrl=")) {
|
||||||
|
// attempt to resolve url from warning page
|
||||||
|
query.remove(0, 10);
|
||||||
|
ResourcePackResourcePage::openUrl({QUrl::fromPercentEncoding(query.toUtf8())}); // double decoding is necessary
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TexturePackResourcePage::openUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
// I don't know why, but doing this on the parent class makes it so that
|
// I don't know why, but doing this on the parent class makes it so that
|
||||||
// other mod providers start loading before being selected, at least with
|
// other mod providers start loading before being selected, at least with
|
||||||
// my Qt, so we need to implement this in every derived class...
|
// my Qt, so we need to implement this in every derived class...
|
||||||
auto FlameModPage::shouldDisplay() const -> bool { return true; }
|
auto FlameModPage::shouldDisplay() const -> bool { return true; }
|
||||||
auto FlameResourcePackPage::shouldDisplay() const -> bool { return true; }
|
auto FlameResourcePackPage::shouldDisplay() const -> bool { return true; }
|
||||||
|
auto FlameTexturePackPage::shouldDisplay() const -> bool { return true; }
|
||||||
|
|
||||||
} // namespace ResourceDownload
|
} // namespace ResourceDownload
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
#include "ui/pages/modplatform/ModPage.h"
|
#include "ui/pages/modplatform/ModPage.h"
|
||||||
#include "ui/pages/modplatform/ResourcePackPage.h"
|
#include "ui/pages/modplatform/ResourcePackPage.h"
|
||||||
#include "ui/pages/modplatform/ShaderPackPage.h"
|
#include "ui/pages/modplatform/TexturePackPage.h"
|
||||||
|
|
||||||
namespace ResourceDownload {
|
namespace ResourceDownload {
|
||||||
|
|
||||||
@ -111,4 +111,31 @@ class FlameResourcePackPage : public ResourcePackResourcePage {
|
|||||||
void openUrl(const QUrl& url) override;
|
void openUrl(const QUrl& url) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class FlameTexturePackPage : public TexturePackResourcePage {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
static FlameTexturePackPage* create(TexturePackDownloadDialog* dialog, BaseInstance& instance)
|
||||||
|
{
|
||||||
|
return TexturePackResourcePage::create<FlameTexturePackPage>(dialog, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlameTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance);
|
||||||
|
~FlameTexturePackPage() override = default;
|
||||||
|
|
||||||
|
[[nodiscard]] bool shouldDisplay() const override;
|
||||||
|
|
||||||
|
[[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); }
|
||||||
|
[[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); }
|
||||||
|
[[nodiscard]] inline auto id() const -> QString override { return Flame::id(); }
|
||||||
|
[[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); }
|
||||||
|
[[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); }
|
||||||
|
|
||||||
|
[[nodiscard]] inline auto helpPage() const -> QString override { return ""; }
|
||||||
|
|
||||||
|
bool optedOut(ModPlatform::IndexedVersion& ver) const override;
|
||||||
|
|
||||||
|
void openUrl(const QUrl& url) override;
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace ResourceDownload
|
} // namespace ResourceDownload
|
||||||
|
@ -69,6 +69,28 @@ auto ModrinthResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJs
|
|||||||
return obj.object().value("hits").toArray();
|
return obj.object().value("hits").toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ModrinthTexturePackModel::ModrinthTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new ModrinthAPI){}
|
||||||
|
|
||||||
|
void ModrinthTexturePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
|
||||||
|
{
|
||||||
|
::Modrinth::loadIndexedPack(m, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModrinthTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
|
||||||
|
{
|
||||||
|
::Modrinth::loadExtraPackData(m, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModrinthTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
|
||||||
|
{
|
||||||
|
::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ModrinthTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
|
||||||
|
{
|
||||||
|
return obj.object().value("hits").toArray();
|
||||||
|
}
|
||||||
|
|
||||||
ModrinthShaderPackModel::ModrinthShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new ModrinthAPI){}
|
ModrinthShaderPackModel::ModrinthShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new ModrinthAPI){}
|
||||||
|
|
||||||
void ModrinthShaderPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
|
void ModrinthShaderPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
|
||||||
|
@ -62,6 +62,24 @@ class ModrinthResourcePackModel : public ResourcePackResourceModel {
|
|||||||
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
|
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class ModrinthTexturePackModel : public TexturePackResourceModel {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
ModrinthTexturePackModel(const BaseInstance&);
|
||||||
|
~ModrinthTexturePackModel() override = default;
|
||||||
|
|
||||||
|
private:
|
||||||
|
[[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; }
|
||||||
|
[[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); }
|
||||||
|
|
||||||
|
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
|
||||||
|
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
|
||||||
|
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
|
||||||
|
|
||||||
|
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
|
||||||
|
};
|
||||||
|
|
||||||
class ModrinthShaderPackModel : public ShaderPackResourceModel {
|
class ModrinthShaderPackModel : public ShaderPackResourceModel {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
|
@ -100,6 +100,24 @@ ModrinthResourcePackPage::ModrinthResourcePackPage(ResourcePackDownloadDialog* d
|
|||||||
m_ui->packDescription->setMetaEntry(metaEntryBase());
|
m_ui->packDescription->setMetaEntry(metaEntryBase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ModrinthTexturePackPage::ModrinthTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance)
|
||||||
|
: TexturePackResourcePage(dialog, instance)
|
||||||
|
{
|
||||||
|
m_model = new ModrinthTexturePackModel(instance);
|
||||||
|
m_ui->packView->setModel(m_model);
|
||||||
|
|
||||||
|
addSortings();
|
||||||
|
|
||||||
|
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
|
||||||
|
// so it's best not to connect them in the parent's constructor...
|
||||||
|
connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
|
||||||
|
connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthTexturePackPage::onSelectionChanged);
|
||||||
|
connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthTexturePackPage::onVersionSelectionChanged);
|
||||||
|
connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthTexturePackPage::onResourceSelected);
|
||||||
|
|
||||||
|
m_ui->packDescription->setMetaEntry(metaEntryBase());
|
||||||
|
}
|
||||||
|
|
||||||
ModrinthShaderPackPage::ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance)
|
ModrinthShaderPackPage::ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance)
|
||||||
: ShaderPackResourcePage(dialog, instance)
|
: ShaderPackResourcePage(dialog, instance)
|
||||||
{
|
{
|
||||||
@ -123,6 +141,7 @@ ModrinthShaderPackPage::ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog,
|
|||||||
// my Qt, so we need to implement this in every derived class...
|
// my Qt, so we need to implement this in every derived class...
|
||||||
auto ModrinthModPage::shouldDisplay() const -> bool { return true; }
|
auto ModrinthModPage::shouldDisplay() const -> bool { return true; }
|
||||||
auto ModrinthResourcePackPage::shouldDisplay() const -> bool { return true; }
|
auto ModrinthResourcePackPage::shouldDisplay() const -> bool { return true; }
|
||||||
|
auto ModrinthTexturePackPage::shouldDisplay() const -> bool { return true; }
|
||||||
auto ModrinthShaderPackPage::shouldDisplay() const -> bool { return true; }
|
auto ModrinthShaderPackPage::shouldDisplay() const -> bool { return true; }
|
||||||
|
|
||||||
} // namespace ResourceDownload
|
} // namespace ResourceDownload
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
|
|
||||||
#include "ui/pages/modplatform/ModPage.h"
|
#include "ui/pages/modplatform/ModPage.h"
|
||||||
#include "ui/pages/modplatform/ResourcePackPage.h"
|
#include "ui/pages/modplatform/ResourcePackPage.h"
|
||||||
|
#include "ui/pages/modplatform/TexturePackPage.h"
|
||||||
#include "ui/pages/modplatform/ShaderPackPage.h"
|
#include "ui/pages/modplatform/ShaderPackPage.h"
|
||||||
|
|
||||||
namespace ResourceDownload {
|
namespace ResourceDownload {
|
||||||
@ -103,6 +104,29 @@ class ModrinthResourcePackPage : public ResourcePackResourcePage {
|
|||||||
[[nodiscard]] inline auto helpPage() const -> QString override { return ""; }
|
[[nodiscard]] inline auto helpPage() const -> QString override { return ""; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class ModrinthTexturePackPage : public TexturePackResourcePage {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
static ModrinthTexturePackPage* create(TexturePackDownloadDialog* dialog, BaseInstance& instance)
|
||||||
|
{
|
||||||
|
return TexturePackResourcePage::create<ModrinthTexturePackPage>(dialog, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
ModrinthTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance);
|
||||||
|
~ModrinthTexturePackPage() override = default;
|
||||||
|
|
||||||
|
[[nodiscard]] bool shouldDisplay() const override;
|
||||||
|
|
||||||
|
[[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); }
|
||||||
|
[[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); }
|
||||||
|
[[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); }
|
||||||
|
[[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); }
|
||||||
|
[[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); }
|
||||||
|
|
||||||
|
[[nodiscard]] inline auto helpPage() const -> QString override { return ""; }
|
||||||
|
};
|
||||||
|
|
||||||
class ModrinthShaderPackPage : public ShaderPackResourcePage {
|
class ModrinthShaderPackPage : public ShaderPackResourcePage {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user