Data pack management

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad
2023-11-28 12:30:13 +00:00
parent 40cfa145ee
commit 284e536e81
29 changed files with 943 additions and 15 deletions

View File

@ -683,6 +683,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("RPDownloadGeometry", "");
m_settings->registerSetting("TPDownloadGeometry", "");
m_settings->registerSetting("ShaderDownloadGeometry", "");
m_settings->registerSetting("DataPackDownloadGeometry", "");
// data pack window
// in future, more pages may be added - so this name is chosen to avoid needing migration
m_settings->registerSetting("WorldManagementGeometry", "");
// HACK: This code feels so stupid is there a less stupid way of doing this?
{

View File

@ -333,6 +333,8 @@ set(MINECRAFT_SOURCES
minecraft/mod/ResourceFolderModel.cpp
minecraft/mod/DataPack.h
minecraft/mod/DataPack.cpp
minecraft/mod/DataPackFolderModel.h
minecraft/mod/DataPackFolderModel.cpp
minecraft/mod/ResourcePack.h
minecraft/mod/ResourcePack.cpp
minecraft/mod/ResourcePackFolderModel.h
@ -861,6 +863,8 @@ SET(LAUNCHER_SOURCES
ui/pages/instance/VersionPage.h
ui/pages/instance/ManagedPackPage.cpp
ui/pages/instance/ManagedPackPage.h
ui/pages/instance/DataPackPage.h
ui/pages/instance/DataPackPage.cpp
ui/pages/instance/TexturePackPage.h
ui/pages/instance/TexturePackPage.cpp
ui/pages/instance/ResourcePackPage.h
@ -930,6 +934,9 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/ShaderPackPage.cpp
ui/pages/modplatform/ShaderPackModel.cpp
ui/pages/modplatform/DataPackPage.cpp
ui/pages/modplatform/DataPackModel.cpp
ui/pages/modplatform/atlauncher/AtlFilterModel.cpp
ui/pages/modplatform/atlauncher/AtlFilterModel.h
ui/pages/modplatform/atlauncher/AtlListModel.cpp

View File

@ -25,7 +25,9 @@
#include <QMap>
#include <QRegularExpression>
#include "MTPixmapCache.h"
#include "Version.h"
#include "minecraft/mod/tasks/LocalDataPackParseTask.h"
// Values taken from:
// https://minecraft.wiki/w/Tutorials/Creating_a_data_pack#%22pack_format%22
@ -56,6 +58,51 @@ void DataPack::setDescription(QString new_description)
m_description = new_description;
}
void DataPack::setImage(QImage new_image) const
{
QMutexLocker locker(&m_data_lock);
Q_ASSERT(!new_image.isNull());
if (m_pack_image_cache_key.key.isValid())
PixmapCache::instance().remove(m_pack_image_cache_key.key);
// scale the image to avoid flooding the pixmapcache
auto pixmap =
QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap);
m_pack_image_cache_key.was_ever_used = true;
// This can happen if the pixmap is too big to fit in the cache :c
if (!m_pack_image_cache_key.key.isValid()) {
qWarning() << "Could not insert a image cache entry! Ignoring it.";
m_pack_image_cache_key.was_ever_used = false;
}
}
QPixmap DataPack::image(QSize size, Qt::AspectRatioMode mode) const
{
QPixmap cached_image;
if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) {
if (size.isNull())
return cached_image;
return cached_image.scaled(size, mode, Qt::SmoothTransformation);
}
// No valid image we can get
if (!m_pack_image_cache_key.was_ever_used) {
return {};
} else {
qDebug() << "Resource Pack" << name() << "Had it's image evicted from the cache. reloading...";
PixmapCache::markCacheMissByEviciton();
}
// Imaged got evicted from the cache. Re-process it and retry.
DataPackUtils::processPackPNG(*this);
return image(size);
}
std::pair<Version, Version> DataPack::compatibleVersions() const
{
if (!s_pack_format_versions.contains(m_pack_format)) {

View File

@ -24,6 +24,7 @@
#include "Resource.h"
#include <QMutex>
#include <QPixmapCache>
class Version;
@ -48,12 +49,18 @@ class DataPack : public Resource {
/** Gets the description of the data pack. */
[[nodiscard]] QString description() const { return m_description; }
/** Gets the image of the resource pack, converted to a QPixmap for drawing, and scaled to size. */
[[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const;
/** Thread-safe. */
void setPackFormat(int new_format_id);
/** Thread-safe. */
void setDescription(QString new_description);
/** Thread-safe. */
void setImage(QImage new_image) const;
bool valid() const override;
[[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override;
@ -70,4 +77,14 @@ class DataPack : public Resource {
/** The data pack's description, as defined in the pack.mcmeta file.
*/
QString m_description;
/** The data pack's image file cache key, for access in the QPixmapCache global instance.
*
* The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true),
* so as to tell whether a cache entry is inexistent or if it was just evicted from the cache.
*/
struct {
QPixmapCache::Key key;
bool was_ever_used = false;
} mutable m_pack_image_cache_key;
};

View File

@ -0,0 +1,191 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* 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 "DataPackFolderModel.h"
#include <qnamespace.h>
#include <qsize.h>
#include <QIcon>
#include <QStyle>
#include "Application.h"
#include "Version.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
#include "minecraft/mod/tasks/LocalDataPackParseTask.h"
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
DataPackFolderModel::DataPackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance)
{
m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" });
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE };
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true };
}
QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const
{
if (!validateIndex(index))
return {};
int row = index.row();
int column = index.column();
switch (role) {
case Qt::DisplayRole:
switch (column) {
case NameColumn:
return m_resources[row]->name();
case PackFormatColumn: {
auto resource = at(row);
auto pack_format = resource->packFormat();
if (pack_format == 0)
return tr("Unrecognized");
auto version_bounds = resource->compatibleVersions();
if (version_bounds.first.toString().isEmpty())
return QString::number(pack_format);
return QString("%1 (%2 - %3)")
.arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString());
}
case DateColumn:
return m_resources[row]->dateTimeChanged();
default:
return {};
}
case Qt::DecorationRole: {
if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink()))
return APPLICATION->getThemedIcon("status-yellow");
if (column == ImageColumn) {
return at(row)->image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding);
}
return {};
}
case Qt::ToolTipRole: {
if (column == PackFormatColumn) {
//: The string being explained by this is in the format: ID (Lower version - Upper version)
return tr("The data pack format ID, as well as the Minecraft versions it was designed for.");
}
if (column == NameColumn) {
if (at(row)->isSymLinkUnder(instDirPath())) {
return m_resources[row]->internal_id() +
tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original."
"\nCanonical Path: %1")
.arg(at(row)->fileinfo().canonicalFilePath());
;
}
if (at(row)->isMoreThanOneHardLink()) {
return m_resources[row]->internal_id() +
tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original.");
}
}
return m_resources[row]->internal_id();
}
case Qt::SizeHintRole:
if (column == ImageColumn) {
return QSize(32, 32);
}
return {};
case Qt::CheckStateRole:
switch (column) {
case ActiveColumn:
return at(row)->enabled() ? Qt::Checked : Qt::Unchecked;
default:
return {};
}
default:
return {};
}
}
QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const
{
switch (role) {
case Qt::DisplayRole:
switch (section) {
case ActiveColumn:
case NameColumn:
case PackFormatColumn:
case DateColumn:
case ImageColumn:
return columnNames().at(section);
default:
return {};
}
case Qt::ToolTipRole:
switch (section) {
case ActiveColumn:
return tr("Is the data pack enabled? (Only valid for ZIPs)");
case NameColumn:
return tr("The name of the data pack.");
case PackFormatColumn:
//: The string being explained by this is in the format: ID (Lower version - Upper version)
return tr("The data pack format ID, as well as the Minecraft versions it was designed for.");
case DateColumn:
return tr("The date and time this data pack was last changed (or added).");
default:
return {};
}
case Qt::SizeHintRole:
if (section == ImageColumn) {
return QSize(64, 0);
}
return {};
default:
return {};
}
}
int DataPackFolderModel::columnCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : NUM_COLUMNS;
}
Task* DataPackFolderModel::createUpdateTask()
{
return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared<DataPack>(entry); });
}
Task* DataPackFolderModel::createParseTask(Resource& resource)
{
return new LocalDataPackParseTask(m_next_resolution_ticket, static_cast<DataPack&>(resource));
}

View File

@ -0,0 +1,62 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* 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.
*/
#pragma once
#include "ResourceFolderModel.h"
#include "DataPack.h"
#include "ResourcePack.h"
class DataPackFolderModel : public ResourceFolderModel {
Q_OBJECT
public:
enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, NUM_COLUMNS };
explicit DataPackFolderModel(const QString& dir, BaseInstance* instance);
virtual QString id() const override { return "datapacks"; }
[[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
[[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
[[nodiscard]] int columnCount(const QModelIndex& parent) const override;
[[nodiscard]] Task* createUpdateTask() override;
[[nodiscard]] Task* createParseTask(Resource&) override;
RESOURCE_HELPERS(DataPack)
};

View File

@ -81,6 +81,29 @@ bool processFolder(DataPack& pack, ProcessingLevel level)
return true; // only need basic info already checked
}
auto png_invalid = [&pack]() {
qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png";
return true; // the png is optional
};
QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png"));
if (image_file_info.exists() && image_file_info.isFile()) {
QFile pack_png_file(image_file_info.filePath());
if (!pack_png_file.open(QIODevice::ReadOnly))
return png_invalid(); // can't open pack.png file
auto data = pack_png_file.readAll();
bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data));
pack_png_file.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
return png_invalid(); // pack.png does not exists or is not a valid file.
}
return true; // all tests passed
}
@ -128,6 +151,32 @@ bool processZIP(DataPack& pack, ProcessingLevel level)
return true; // only need basic info already checked
}
auto png_invalid = [&pack]() {
qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png";
return true; // the png is optional
};
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return png_invalid();
}
auto data = file.readAll();
bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data));
file.close();
zip.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
zip.close();
return png_invalid(); // could not set pack.mcmeta as current file.
}
zip.close();
return true;
@ -149,6 +198,78 @@ bool processMCMeta(DataPack& pack, QByteArray&& raw_data)
return true;
}
bool processPackPNG(const DataPack& pack, QByteArray&& raw_data)
{
auto img = QImage::fromData(raw_data);
if (!img.isNull()) {
pack.setImage(img);
} else {
qWarning() << "Failed to parse pack.png.";
return false;
}
return true;
}
bool processPackPNG(const DataPack& pack)
{
auto png_invalid = [&pack]() {
qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png";
return false;
};
switch (pack.type()) {
case ResourceType::FOLDER: {
QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png"));
if (image_file_info.exists() && image_file_info.isFile()) {
QFile pack_png_file(image_file_info.filePath());
if (!pack_png_file.open(QIODevice::ReadOnly))
return png_invalid(); // can't open pack.png file
auto data = pack_png_file.readAll();
bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data));
pack_png_file.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
return png_invalid(); // pack.png does not exists or is not a valid file.
}
return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740
}
case ResourceType::ZIPFILE: {
QuaZip zip(pack.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
return false; // can't open zip file
QuaZipFile file(&zip);
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return png_invalid();
}
auto data = file.readAll();
bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data));
file.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
return png_invalid(); // could not set pack.mcmeta as current file.
}
return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740
}
default:
qWarning() << "Invalid type for data pack parse task!";
return false;
}
}
bool validate(QFileInfo file)
{
DataPack dp{ file };

View File

@ -38,6 +38,10 @@ bool processZIP(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool processFolder(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool processMCMeta(DataPack& pack, QByteArray&& raw_data);
bool processPackPNG(const DataPack& pack, QByteArray&& raw_data);
/// processes ONLY the pack.png (rest of the pack may be invalid)
bool processPackPNG(const DataPack& pack);
/** Checks whether a file is valid as a data pack or not. */
bool validate(QFileInfo file);

View File

@ -36,7 +36,7 @@ Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType)
enum class ResourceProvider { MODRINTH, FLAME };
enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK };
enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK, DATA_PACK };
enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN };

View File

@ -37,6 +37,7 @@ class FlameAPI : public NetworkResourceAPI {
case ModPlatform::ResourceType::MOD:
return 6;
case ModPlatform::ResourceType::RESOURCE_PACK:
case ModPlatform::ResourceType::DATA_PACK:
return 12;
case ModPlatform::ResourceType::SHADER_PACK:
return 6552;

View File

@ -61,6 +61,7 @@ class ModrinthAPI : public NetworkResourceAPI {
{
switch (type) {
case ModPlatform::ResourceType::MOD:
case ModPlatform::ResourceType::DATA_PACK:
return "mod";
case ModPlatform::ResourceType::RESOURCE_PACK:
return "resourcepack";
@ -81,6 +82,8 @@ class ModrinthAPI : public NetworkResourceAPI {
facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value())));
if (args.versions.has_value())
facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value())));
if (args.type == ModPlatform::ResourceType::DATA_PACK)
facets_list.append("[\"categories:datapack\"]");
facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type)));
return QString("[%1]").arg(facets_list.join(','));

View File

@ -57,7 +57,7 @@ ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::share
{
setObjectName(QStringLiteral("ResourceDownloadDialog"));
resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0));
resize(static_cast<int>(std::max(0.5 * parent->width(), 400.0)), static_cast<int>(std::max(0.75 * parent->height(), 400.0)));
setWindowIcon(APPLICATION->getThemedIcon("new"));
@ -356,4 +356,25 @@ QList<BasePage*> ShaderPackDownloadDialog::getPages()
return pages;
}
DataPackDownloadDialog::DataPackDownloadDialog(QWidget* parent,
const std::shared_ptr<DataPackFolderModel>& data_packs,
BaseInstance* instance)
: ResourceDownloadDialog(parent, data_packs), m_instance(instance)
{
setWindowTitle(dialogTitle());
initializeContainer();
connectButtons();
if (!geometrySaveKey().isEmpty())
restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray()));
}
QList<BasePage*> DataPackDownloadDialog::getPages()
{
QList<BasePage*> pages;
pages.append(ModrinthDataPackPage::create(this, *m_instance));
return pages;
}
} // namespace ResourceDownload

View File

@ -25,6 +25,7 @@
#include <QLayout>
#include "QObjectPtr.h"
#include "minecraft/mod/DataPackFolderModel.h"
#include "minecraft/mod/tasks/GetModDependenciesTask.h"
#include "modplatform/ModIndex.h"
#include "ui/pages/BasePageProvider.h"
@ -166,4 +167,21 @@ class ShaderPackDownloadDialog final : public ResourceDownloadDialog {
BaseInstance* m_instance;
};
class DataPackDownloadDialog final : public ResourceDownloadDialog {
Q_OBJECT
public:
explicit DataPackDownloadDialog(QWidget* parent, const std::shared_ptr<DataPackFolderModel>& data_packs, BaseInstance* instance);
~DataPackDownloadDialog() override = default;
//: String that gets appended to the data pack download dialog title ("Download " + resourcesString())
[[nodiscard]] QString resourcesString() const override { return tr("data packs"); }
[[nodiscard]] QString geometrySaveKey() const override { return "DataPackDownloadGeometry"; }
QList<BasePage*> getPages() override;
private:
BaseInstance* m_instance;
};
} // namespace ResourceDownload

View File

@ -0,0 +1,82 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* 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/>.
*/
#include "DataPackPage.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
DataPackPage::DataPackPage(MinecraftInstance* instance, std::shared_ptr<DataPackFolderModel> model, QWidget* parent)
: ExternalResourcesPage(instance, model, parent)
{
ui->actionDownloadItem->setText(tr("Download packs"));
ui->actionDownloadItem->setToolTip(tr("Download data packs from online platforms"));
ui->actionDownloadItem->setEnabled(true);
connect(ui->actionDownloadItem, &QAction::triggered, this, &DataPackPage::downloadDataPacks);
ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem);
ui->actionViewConfigs->setVisible(false);
}
bool DataPackPage::onSelectionChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous)
{
auto sourceCurrent = m_filterModel->mapToSource(current);
int row = sourceCurrent.row();
auto& dp = static_cast<DataPack&>(m_model->at(row));
ui->frame->updateWithDataPack(dp);
return true;
}
void DataPackPage::downloadDataPacks()
{
if (m_instance->typeName() != "Minecraft")
return; // this is a null instance or a legacy instance
ResourceDownload::DataPackDownloadDialog mdownload(this, std::static_pointer_cast<DataPackFolderModel>(m_model), m_instance);
if (mdownload.exec()) {
auto tasks =
new ConcurrentTask(this, "Download Data Pack", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt());
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();
}
}

View File

@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* 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/>.
*/
#pragma once
#include "ExternalResourcesPage.h"
#include "minecraft/mod/DataPackFolderModel.h"
#include "ui_ExternalResourcesPage.h"
class DataPackPage : public ExternalResourcesPage {
Q_OBJECT
public:
explicit DataPackPage(MinecraftInstance* instance, std::shared_ptr<DataPackFolderModel> model, QWidget* parent = 0);
QString displayName() const override { return tr("Data packs"); }
QIcon icon() const override { return APPLICATION->getThemedIcon("datapacks"); }
QString id() const override { return "datapacks"; }
QString helpPage() const override { return "Data-packs"; }
bool shouldDisplay() const override { return true; }
public slots:
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override;
void downloadDataPacks();
};

View File

@ -1,5 +1,6 @@
#pragma once
#include <QDialog>
#include <QMainWindow>
#include <QSortFilterProxyModel>

View File

@ -57,6 +57,7 @@
#include "ui/GuiUtil.h"
#include "Application.h"
#include "DataPackPage.h"
class WorldListProxyModel : public QSortFilterProxyModel {
Q_OBJECT
@ -82,7 +83,7 @@ class WorldListProxyModel : public QSortFilterProxyModel {
}
};
WorldListPage::WorldListPage(BaseInstance* inst, std::shared_ptr<WorldList> worlds, QWidget* parent)
WorldListPage::WorldListPage(MinecraftInstance* inst, std::shared_ptr<WorldList> worlds, QWidget* parent)
: QMainWindow(parent), m_inst(inst), ui(new Ui::WorldListPage), m_worlds(worlds)
{
ui->setupUi(this);
@ -210,7 +211,7 @@ void WorldListPage::on_actionView_Folder_triggered()
DesktopServices::openDirectory(m_worlds->dir().absolutePath(), true);
}
void WorldListPage::on_actionDatapacks_triggered()
void WorldListPage::on_actionData_Packs_triggered()
{
QModelIndex index = getSelectedWorld();
@ -218,12 +219,33 @@ void WorldListPage::on_actionDatapacks_triggered()
return;
}
if (!worldSafetyNagQuestion(tr("Open World Datapacks Folder")))
if (!worldSafetyNagQuestion(tr("Manage Data Packs")))
return;
auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString();
const QString fullPath = m_worlds->data(index, WorldList::FolderRole).toString();
const QString folder = FS::PathCombine(fullPath, "datapacks");
DesktopServices::openDirectory(FS::PathCombine(fullPath, "datapacks"), true);
auto dialog = new QDialog(window());
dialog->setWindowTitle(tr("Data packs for %1").arg(m_worlds->data(index, WorldList::NameRole).toString()));
dialog->setWindowModality(Qt::WindowModal);
dialog->resize(static_cast<int>(std::max(0.5 * window()->width(), 400.0)),
static_cast<int>(std::max(0.75 * window()->height(), 400.0)));
dialog->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("DataPackDownloadGeometry").toByteArray()));
auto layout = new QHBoxLayout(dialog);
auto page = new DataPackPage(m_inst, std::make_shared<DataPackFolderModel>(folder, m_inst));
page->setParent(dialog); // HACK: many pages extend QMainWindow; setting the parent manually prevents them from creating a window.
layout->addWidget(page);
dialog->setLayout(layout);
connect(dialog, &QDialog::finished, this, [dialog, page] {
APPLICATION->settings()->set("DataPackDownloadGeometry", dialog->saveGeometry().toBase64());
page->closed();
});
dialog->show();
page->opened();
}
void WorldListPage::on_actionReset_Icon_triggered()
@ -336,7 +358,7 @@ void WorldListPage::worldChanged([[maybe_unused]] const QModelIndex& current, [[
ui->actionRemove->setEnabled(enable);
ui->actionCopy->setEnabled(enable);
ui->actionRename->setEnabled(enable);
ui->actionDatapacks->setEnabled(enable);
ui->actionData_Packs->setEnabled(enable);
bool hasIcon = !index.data(WorldList::IconFileRole).isNull();
ui->actionReset_Icon->setEnabled(enable && hasIcon);
}

View File

@ -53,7 +53,7 @@ class WorldListPage : public QMainWindow, public BasePage {
Q_OBJECT
public:
explicit WorldListPage(BaseInstance* inst, std::shared_ptr<WorldList> worlds, QWidget* parent = 0);
explicit WorldListPage(MinecraftInstance* inst, std::shared_ptr<WorldList> worlds, QWidget* parent = 0);
virtual ~WorldListPage();
virtual QString displayName() const override { return tr("Worlds"); }
@ -72,7 +72,7 @@ class WorldListPage : public QMainWindow, public BasePage {
QMenu* createPopupMenu() override;
protected:
BaseInstance* m_inst;
MinecraftInstance* m_inst;
private:
QModelIndex getSelectedWorld();
@ -97,7 +97,7 @@ class WorldListPage : public QMainWindow, public BasePage {
void on_actionRename_triggered();
void on_actionRefresh_triggered();
void on_actionView_Folder_triggered();
void on_actionDatapacks_triggered();
void on_actionData_Packs_triggered();
void on_actionReset_Icon_triggered();
void worldChanged(const QModelIndex& current, const QModelIndex& previous);
void mceditState(LoggedProcess::State state);

View File

@ -85,7 +85,7 @@
<addaction name="actionCopy"/>
<addaction name="actionRemove"/>
<addaction name="actionMCEdit"/>
<addaction name="actionDatapacks"/>
<addaction name="actionData_Packs"/>
<addaction name="actionReset_Icon"/>
<addaction name="separator"/>
<addaction name="actionCopy_Seed"/>
@ -140,12 +140,12 @@
<string>Remove world icon to make the game re-generate it on next load.</string>
</property>
</action>
<action name="actionDatapacks">
<action name="actionData_Packs">
<property name="text">
<string>Datapacks</string>
<string>Data Packs</string>
</property>
<property name="toolTip">
<string>Manage datapacks inside the world.</string>
<string>Manage data packs inside the world.</string>
</property>
</action>
</widget>

View File

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
// SPDX-FileCopyrightText: 2023 TheKodeToad <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "DataPackModel.h"
#include <QMessageBox>
namespace ResourceDownload {
DataPackResourceModel::DataPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api)
: ResourceModel(api), m_base_instance(base_inst)
{}
/******** Make data requests ********/
ResourceAPI::SearchArgs DataPackResourceModel::createSearchArguments()
{
auto sort = getCurrentSortingMethodByIndex();
return { ModPlatform::ResourceType::DATA_PACK, m_next_search_offset, m_search_term, sort };
}
ResourceAPI::VersionSearchArgs DataPackResourceModel::createVersionsArguments(QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
return { *pack };
}
ResourceAPI::ProjectInfoArgs DataPackResourceModel::createInfoArguments(QModelIndex& entry)
{
auto& pack = m_packs[entry.row()];
return { *pack };
}
void DataPackResourceModel::searchWithTerm(const QString& term, unsigned int sort)
{
if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort) {
return;
}
setSearchTerm(term);
m_current_sort_index = sort;
refresh();
}
} // namespace ResourceDownload

View File

@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
// SPDX-FileCopyrightText: 2023 TheKodeToad <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QAbstractListModel>
#include "BaseInstance.h"
#include "modplatform/ModIndex.h"
#include "ui/pages/modplatform/ResourceModel.h"
class Version;
namespace ResourceDownload {
class DataPackResourceModel : public ResourceModel {
Q_OBJECT
public:
DataPackResourceModel(BaseInstance const&, ResourceAPI*);
/* Ask the API for more information */
void searchWithTerm(const QString& term, unsigned int sort);
void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) override = 0;
void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) override = 0;
void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) override = 0;
public slots:
ResourceAPI::SearchArgs createSearchArguments() override;
ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override;
ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override;
protected:
const BaseInstance& m_base_instance;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0;
};
} // namespace ResourceDownload

View File

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
// SPDX-FileCopyrightText: 2023 TheKodeToad <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "DataPackPage.h"
#include "modplatform/ModIndex.h"
#include "ui_ResourcePage.h"
#include "DataPackModel.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
#include <QRegularExpression>
namespace ResourceDownload {
DataPackResourcePage::DataPackResourcePage(DataPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance)
{
connect(m_ui->searchButton, &QPushButton::clicked, this, &DataPackResourcePage::triggerSearch);
connect(m_ui->packView, &QListView::doubleClicked, this, &DataPackResourcePage::onResourceSelected);
}
/******** Callbacks to events in the UI (set up in the derived classes) ********/
void DataPackResourcePage::triggerSearch()
{
m_ui->packView->clearSelection();
m_ui->packDescription->clear();
m_ui->versionSelectionBox->clear();
updateSelectionButton();
static_cast<DataPackResourceModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt());
m_fetch_progress.watch(m_model->activeSearchJob().get());
}
QMap<QString, QString> DataPackResourcePage::urlHandlers() const
{
QMap<QString, QString> map;
map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/resourcepack\\/([^\\/]+)\\/?"), "modrinth");
map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/texture-packs\\/([^\\/]+)\\/?"),
"curseforge");
map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge");
return map;
}
} // namespace ResourceDownload

View File

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
// SPDX-FileCopyrightText: 2023 TheKodeToad <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "ui/pages/modplatform/ResourcePage.h"
#include "ui/pages/modplatform/DataPackModel.h"
namespace Ui {
class ResourcePage;
}
namespace ResourceDownload {
class DataPackDownloadDialog;
class DataPackResourcePage : public ResourcePage {
Q_OBJECT
public:
template <typename T>
static T* create(DataPackDownloadDialog* dialog, BaseInstance& instance)
{
auto page = new T(dialog, instance);
auto model = static_cast<DataPackResourceModel*>(page->getModel());
connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList);
connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi);
return page;
}
//: The plural version of 'data pack'
[[nodiscard]] inline QString resourcesString() const override { return tr("data packs"); }
//: The singular version of 'data packs'
[[nodiscard]] inline QString resourceString() const override { return tr("data pack"); }
[[nodiscard]] bool supportsFiltering() const override { return false; };
[[nodiscard]] QMap<QString, QString> urlHandlers() const override;
protected:
DataPackResourcePage(DataPackDownloadDialog* dialog, BaseInstance& instance);
protected slots:
void triggerSearch() override;
};
} // namespace ResourceDownload

View File

@ -118,4 +118,27 @@ auto ModrinthShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJson
return obj.object().value("hits").toArray();
}
ModrinthDataPackModel::ModrinthDataPackModel(const BaseInstance& base) : DataPackResourceModel(base, new ModrinthAPI) {}
void ModrinthDataPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
::Modrinth::loadIndexedPack(m, obj);
}
void ModrinthDataPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
::Modrinth::loadExtraPackData(m, obj);
}
void ModrinthDataPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance);
}
auto ModrinthDataPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return obj.object().value("hits").toArray();
}
} // namespace ResourceDownload

View File

@ -20,6 +20,7 @@
#pragma once
#include "ui/pages/modplatform/DataPackModel.h"
#include "ui/pages/modplatform/ModModel.h"
#include "ui/pages/modplatform/ResourcePackModel.h"
#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h"
@ -99,4 +100,22 @@ class ModrinthShaderPackModel : public ShaderPackResourceModel {
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;
};
class ModrinthDataPackModel : public DataPackResourceModel {
Q_OBJECT
public:
ModrinthDataPackModel(const BaseInstance&);
~ModrinthDataPackModel() 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;
};
} // namespace ResourceDownload

View File

@ -124,6 +124,24 @@ ModrinthShaderPackPage::ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog,
m_ui->packDescription->setMetaEntry(metaEntryBase());
}
ModrinthDataPackPage::ModrinthDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance)
: DataPackResourcePage(dialog, instance)
{
m_model = new ModrinthDataPackModel(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, &ModrinthDataPackPage::onSelectionChanged);
connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthDataPackPage::onVersionSelectionChanged);
connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthDataPackPage::onResourceSelected);
m_ui->packDescription->setMetaEntry(metaEntryBase());
}
// 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
// my Qt, so we need to implement this in every derived class...
@ -143,5 +161,9 @@ auto ModrinthShaderPackPage::shouldDisplay() const -> bool
{
return true;
}
auto ModrinthDataPackPage::shouldDisplay() const -> bool
{
return true;
}
} // namespace ResourceDownload

View File

@ -41,6 +41,7 @@
#include "modplatform/ResourceAPI.h"
#include "ui/pages/modplatform/DataPackPage.h"
#include "ui/pages/modplatform/ModPage.h"
#include "ui/pages/modplatform/ResourcePackPage.h"
#include "ui/pages/modplatform/ShaderPackPage.h"
@ -166,4 +167,27 @@ class ModrinthShaderPackPage : public ShaderPackResourcePage {
[[nodiscard]] inline auto helpPage() const -> QString override { return ""; }
};
class ModrinthDataPackPage : public DataPackResourcePage {
Q_OBJECT
public:
static ModrinthDataPackPage* create(DataPackDownloadDialog* dialog, BaseInstance& instance)
{
return DataPackResourcePage::create<ModrinthDataPackPage>(dialog, instance);
}
ModrinthDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance);
~ModrinthDataPackPage() 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 ""; }
};
} // namespace ResourceDownload

View File

@ -212,6 +212,12 @@ void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack)
setImage(resource_pack.image({ 64, 64 }));
}
void InfoFrame::updateWithDataPack(DataPack& data_pack) {
setName(renderColorCodes(data_pack.name()));
setDescription(renderColorCodes(data_pack.description()));
setImage(data_pack.image({ 64, 64 }));
}
void InfoFrame::updateWithTexturePack(TexturePack& texture_pack)
{
setName(renderColorCodes(texture_pack.name()));

View File

@ -37,6 +37,7 @@
#include <QFrame>
#include "minecraft/mod/DataPack.h"
#include "minecraft/mod/Mod.h"
#include "minecraft/mod/ResourcePack.h"
#include "minecraft/mod/TexturePack.h"
@ -63,6 +64,7 @@ class InfoFrame : public QFrame {
void updateWithMod(Mod const& m);
void updateWithResource(Resource const& resource);
void updateWithResourcePack(ResourcePack& rp);
void updateWithDataPack(DataPack& rp);
void updateWithTexturePack(TexturePack& tp);
static QString renderColorCodes(QString input);