commit
3dccc38f42
@ -1,8 +1,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
/*
|
/*
|
||||||
* PolyMC - Minecraft Launcher
|
* Prism Launcher - Minecraft Launcher
|
||||||
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
|
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
|
||||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
* 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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -36,6 +37,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QList>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief The Config class holds all the build-time information passed from the build system.
|
* \brief The Config class holds all the build-time information passed from the build system.
|
||||||
@ -160,6 +162,7 @@ class Config {
|
|||||||
|
|
||||||
QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2";
|
QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2";
|
||||||
QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2";
|
QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2";
|
||||||
|
QStringList MODRINTH_MRPACK_HOSTS{"cdn.modrinth.com", "github.com", "raw.githubusercontent.com", "gitlab.com"};
|
||||||
|
|
||||||
QString FLAME_BASE_URL = "https://api.curseforge.com/v1";
|
QString FLAME_BASE_URL = "https://api.curseforge.com/v1";
|
||||||
|
|
||||||
|
@ -525,6 +525,8 @@ set(MODRINTH_SOURCES
|
|||||||
modplatform/modrinth/ModrinthCheckUpdate.h
|
modplatform/modrinth/ModrinthCheckUpdate.h
|
||||||
modplatform/modrinth/ModrinthInstanceCreationTask.cpp
|
modplatform/modrinth/ModrinthInstanceCreationTask.cpp
|
||||||
modplatform/modrinth/ModrinthInstanceCreationTask.h
|
modplatform/modrinth/ModrinthInstanceCreationTask.h
|
||||||
|
modplatform/modrinth/ModrinthPackExportTask.cpp
|
||||||
|
modplatform/modrinth/ModrinthPackExportTask.h
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PACKWIZ_SOURCES
|
set(PACKWIZ_SOURCES
|
||||||
@ -720,6 +722,10 @@ SET(LAUNCHER_SOURCES
|
|||||||
# FIXME: maybe find a better home for this.
|
# FIXME: maybe find a better home for this.
|
||||||
SkinUtils.cpp
|
SkinUtils.cpp
|
||||||
SkinUtils.h
|
SkinUtils.h
|
||||||
|
FileIgnoreProxy.cpp
|
||||||
|
FileIgnoreProxy.h
|
||||||
|
FastFileIconProvider.cpp
|
||||||
|
FastFileIconProvider.h
|
||||||
|
|
||||||
# GUI - setup wizard
|
# GUI - setup wizard
|
||||||
ui/setupwizard/SetupWizard.h
|
ui/setupwizard/SetupWizard.h
|
||||||
@ -900,6 +906,8 @@ SET(LAUNCHER_SOURCES
|
|||||||
ui/dialogs/EditAccountDialog.h
|
ui/dialogs/EditAccountDialog.h
|
||||||
ui/dialogs/ExportInstanceDialog.cpp
|
ui/dialogs/ExportInstanceDialog.cpp
|
||||||
ui/dialogs/ExportInstanceDialog.h
|
ui/dialogs/ExportInstanceDialog.h
|
||||||
|
ui/dialogs/ExportMrPackDialog.cpp
|
||||||
|
ui/dialogs/ExportMrPackDialog.h
|
||||||
ui/dialogs/IconPickerDialog.cpp
|
ui/dialogs/IconPickerDialog.cpp
|
||||||
ui/dialogs/IconPickerDialog.h
|
ui/dialogs/IconPickerDialog.h
|
||||||
ui/dialogs/ImportResourceDialog.cpp
|
ui/dialogs/ImportResourceDialog.cpp
|
||||||
@ -1046,6 +1054,7 @@ qt_wrap_ui(LAUNCHER_UI
|
|||||||
ui/dialogs/ProfileSelectDialog.ui
|
ui/dialogs/ProfileSelectDialog.ui
|
||||||
ui/dialogs/SkinUploadDialog.ui
|
ui/dialogs/SkinUploadDialog.ui
|
||||||
ui/dialogs/ExportInstanceDialog.ui
|
ui/dialogs/ExportInstanceDialog.ui
|
||||||
|
ui/dialogs/ExportMrPackDialog.ui
|
||||||
ui/dialogs/IconPickerDialog.ui
|
ui/dialogs/IconPickerDialog.ui
|
||||||
ui/dialogs/ImportResourceDialog.ui
|
ui/dialogs/ImportResourceDialog.ui
|
||||||
ui/dialogs/MSALoginDialog.ui
|
ui/dialogs/MSALoginDialog.ui
|
||||||
|
47
launcher/FastFileIconProvider.cpp
Normal file
47
launcher/FastFileIconProvider.cpp
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// 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 "FastFileIconProvider.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QStyle>
|
||||||
|
|
||||||
|
QIcon FastFileIconProvider::icon(const QFileInfo& info) const
|
||||||
|
{
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
|
||||||
|
bool link = info.isSymbolicLink() || info.isAlias() || info.isShortcut();
|
||||||
|
#else
|
||||||
|
// in versions prior to 6.4 we don't have access to isAlias
|
||||||
|
bool link = info.isSymLink();
|
||||||
|
#endif
|
||||||
|
QStyle::StandardPixmap icon;
|
||||||
|
|
||||||
|
if (info.isDir()) {
|
||||||
|
if (link)
|
||||||
|
icon = QStyle::SP_DirLinkIcon;
|
||||||
|
else
|
||||||
|
icon = QStyle::SP_DirIcon;
|
||||||
|
} else {
|
||||||
|
if (link)
|
||||||
|
icon = QStyle::SP_FileLinkIcon;
|
||||||
|
else
|
||||||
|
icon = QStyle::SP_FileIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
return QApplication::style()->standardIcon(icon);
|
||||||
|
}
|
26
launcher/FastFileIconProvider.h
Normal file
26
launcher/FastFileIconProvider.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// 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 <QFileIconProvider>
|
||||||
|
|
||||||
|
class FastFileIconProvider : public QFileIconProvider {
|
||||||
|
public:
|
||||||
|
QIcon icon(const QFileInfo& info) const override;
|
||||||
|
};
|
256
launcher/FileIgnoreProxy.cpp
Normal file
256
launcher/FileIgnoreProxy.cpp
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* Prism Launcher - Minecraft Launcher
|
||||||
|
* 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 "FileIgnoreProxy.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QFileSystemModel>
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
#include <QStack>
|
||||||
|
#include "FileSystem.h"
|
||||||
|
#include "SeparatorPrefixTree.h"
|
||||||
|
#include "StringUtils.h"
|
||||||
|
|
||||||
|
FileIgnoreProxy::FileIgnoreProxy(QString root, QObject* parent) : QSortFilterProxyModel(parent), root(root) {}
|
||||||
|
// NOTE: Sadly, we have to do sorting ourselves.
|
||||||
|
bool FileIgnoreProxy::lessThan(const QModelIndex& left, const QModelIndex& right) const
|
||||||
|
{
|
||||||
|
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
|
||||||
|
if (!fsm) {
|
||||||
|
return QSortFilterProxyModel::lessThan(left, right);
|
||||||
|
}
|
||||||
|
bool asc = sortOrder() == Qt::AscendingOrder ? true : false;
|
||||||
|
|
||||||
|
QFileInfo leftFileInfo = fsm->fileInfo(left);
|
||||||
|
QFileInfo rightFileInfo = fsm->fileInfo(right);
|
||||||
|
|
||||||
|
if (!leftFileInfo.isDir() && rightFileInfo.isDir()) {
|
||||||
|
return !asc;
|
||||||
|
}
|
||||||
|
if (leftFileInfo.isDir() && !rightFileInfo.isDir()) {
|
||||||
|
return asc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort and proxy model breaks the original model...
|
||||||
|
if (sortColumn() == 0) {
|
||||||
|
return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0;
|
||||||
|
}
|
||||||
|
if (sortColumn() == 1) {
|
||||||
|
auto leftSize = leftFileInfo.size();
|
||||||
|
auto rightSize = rightFileInfo.size();
|
||||||
|
if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir())) {
|
||||||
|
return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0 ? asc : !asc;
|
||||||
|
}
|
||||||
|
return leftSize < rightSize;
|
||||||
|
}
|
||||||
|
return QSortFilterProxyModel::lessThan(left, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
Qt::ItemFlags FileIgnoreProxy::flags(const QModelIndex& index) const
|
||||||
|
{
|
||||||
|
if (!index.isValid())
|
||||||
|
return Qt::NoItemFlags;
|
||||||
|
|
||||||
|
auto sourceIndex = mapToSource(index);
|
||||||
|
Qt::ItemFlags flags = sourceIndex.flags();
|
||||||
|
if (index.column() == 0) {
|
||||||
|
flags |= Qt::ItemIsUserCheckable;
|
||||||
|
if (sourceIndex.model()->hasChildren(sourceIndex)) {
|
||||||
|
flags |= Qt::ItemIsAutoTristate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant FileIgnoreProxy::data(const QModelIndex& index, int role) const
|
||||||
|
{
|
||||||
|
QModelIndex sourceIndex = mapToSource(index);
|
||||||
|
|
||||||
|
if (index.column() == 0 && role == Qt::CheckStateRole) {
|
||||||
|
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
|
||||||
|
auto blockedPath = relPath(fsm->filePath(sourceIndex));
|
||||||
|
auto cover = blocked.cover(blockedPath);
|
||||||
|
if (!cover.isNull()) {
|
||||||
|
return QVariant(Qt::Unchecked);
|
||||||
|
} else if (blocked.exists(blockedPath)) {
|
||||||
|
return QVariant(Qt::PartiallyChecked);
|
||||||
|
} else {
|
||||||
|
return QVariant(Qt::Checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sourceIndex.data(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileIgnoreProxy::setData(const QModelIndex& index, const QVariant& value, int role)
|
||||||
|
{
|
||||||
|
if (index.column() == 0 && role == Qt::CheckStateRole) {
|
||||||
|
Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
|
||||||
|
return setFilterState(index, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
QModelIndex sourceIndex = mapToSource(index);
|
||||||
|
return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FileIgnoreProxy::relPath(const QString& path) const
|
||||||
|
{
|
||||||
|
return QDir(root).relativeFilePath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state)
|
||||||
|
{
|
||||||
|
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
|
||||||
|
|
||||||
|
if (!fsm) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QModelIndex sourceIndex = mapToSource(index);
|
||||||
|
auto blockedPath = relPath(fsm->filePath(sourceIndex));
|
||||||
|
bool changed = false;
|
||||||
|
if (state == Qt::Unchecked) {
|
||||||
|
// blocking a path
|
||||||
|
auto& node = blocked.insert(blockedPath);
|
||||||
|
// get rid of all blocked nodes below
|
||||||
|
node.clear();
|
||||||
|
changed = true;
|
||||||
|
} else if (state == Qt::Checked || state == Qt::PartiallyChecked) {
|
||||||
|
if (!blocked.remove(blockedPath)) {
|
||||||
|
auto cover = blocked.cover(blockedPath);
|
||||||
|
qDebug() << "Blocked by cover" << cover;
|
||||||
|
// uncover
|
||||||
|
blocked.remove(cover);
|
||||||
|
// block all contents, except for any cover
|
||||||
|
QModelIndex rootIndex = fsm->index(FS::PathCombine(root, cover));
|
||||||
|
QModelIndex doing = rootIndex;
|
||||||
|
int row = 0;
|
||||||
|
QStack<QModelIndex> todo;
|
||||||
|
while (1) {
|
||||||
|
auto node = fsm->index(row, 0, doing);
|
||||||
|
if (!node.isValid()) {
|
||||||
|
if (!todo.size()) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
doing = todo.pop();
|
||||||
|
row = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto relpath = relPath(fsm->filePath(node));
|
||||||
|
if (blockedPath.startsWith(relpath)) // cover found?
|
||||||
|
{
|
||||||
|
// continue processing cover later
|
||||||
|
todo.push(node);
|
||||||
|
} else {
|
||||||
|
// or just block this one.
|
||||||
|
blocked.insert(relpath);
|
||||||
|
}
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
// update the thing
|
||||||
|
emit dataChanged(index, index, { Qt::CheckStateRole });
|
||||||
|
// update everything above index
|
||||||
|
QModelIndex up = index.parent();
|
||||||
|
while (1) {
|
||||||
|
if (!up.isValid())
|
||||||
|
break;
|
||||||
|
emit dataChanged(up, up, { Qt::CheckStateRole });
|
||||||
|
up = up.parent();
|
||||||
|
}
|
||||||
|
// and everything below the index
|
||||||
|
QModelIndex doing = index;
|
||||||
|
int row = 0;
|
||||||
|
QStack<QModelIndex> todo;
|
||||||
|
while (1) {
|
||||||
|
auto node = this->index(row, 0, doing);
|
||||||
|
if (!node.isValid()) {
|
||||||
|
if (!todo.size()) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
doing = todo.pop();
|
||||||
|
row = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit dataChanged(node, node, { Qt::CheckStateRole });
|
||||||
|
todo.push(node);
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
// siblings and unrelated nodes are ignored
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileIgnoreProxy::shouldExpand(QModelIndex index)
|
||||||
|
{
|
||||||
|
QModelIndex sourceIndex = mapToSource(index);
|
||||||
|
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
|
||||||
|
if (!fsm) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto blockedPath = relPath(fsm->filePath(sourceIndex));
|
||||||
|
auto found = blocked.find(blockedPath);
|
||||||
|
if (found) {
|
||||||
|
return !found->leaf();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileIgnoreProxy::setBlockedPaths(QStringList paths)
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
|
blocked.clear();
|
||||||
|
blocked.insert(paths);
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileIgnoreProxy::filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(source_parent)
|
||||||
|
|
||||||
|
// adjust the columns you want to filter out here
|
||||||
|
// return false for those that will be hidden
|
||||||
|
if (source_column == 2 || source_column == 3)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
72
launcher/FileIgnoreProxy.h
Normal file
72
launcher/FileIgnoreProxy.h
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* Prism Launcher - Minecraft Launcher
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
#include "SeparatorPrefixTree.h"
|
||||||
|
|
||||||
|
class FileIgnoreProxy : public QSortFilterProxyModel {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
FileIgnoreProxy(QString root, QObject* parent);
|
||||||
|
// NOTE: Sadly, we have to do sorting ourselves.
|
||||||
|
bool lessThan(const QModelIndex& left, const QModelIndex& right) const;
|
||||||
|
|
||||||
|
virtual Qt::ItemFlags flags(const QModelIndex& index) const;
|
||||||
|
|
||||||
|
virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
|
||||||
|
virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole);
|
||||||
|
|
||||||
|
QString relPath(const QString& path) const;
|
||||||
|
|
||||||
|
bool setFilterState(QModelIndex index, Qt::CheckState state);
|
||||||
|
|
||||||
|
bool shouldExpand(QModelIndex index);
|
||||||
|
|
||||||
|
void setBlockedPaths(QStringList paths);
|
||||||
|
|
||||||
|
inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return blocked; }
|
||||||
|
inline SeparatorPrefixTree<'/'>& blockedPaths() { return blocked; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const QString root;
|
||||||
|
SeparatorPrefixTree<'/'> blocked;
|
||||||
|
};
|
323
launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
Normal file
323
launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
// 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 "ModrinthPackExportTask.h"
|
||||||
|
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QtConcurrentRun>
|
||||||
|
#include "Json.h"
|
||||||
|
#include "MMCZip.h"
|
||||||
|
#include "minecraft/PackProfile.h"
|
||||||
|
#include "minecraft/mod/ModFolderModel.h"
|
||||||
|
|
||||||
|
const QStringList ModrinthPackExportTask::PREFIXES({ "mods", "coremods", "resourcepacks", "texturepacks", "shaderpacks" });
|
||||||
|
const QStringList ModrinthPackExportTask::FILE_EXTENSIONS({ "jar", "litemod", "zip" });
|
||||||
|
|
||||||
|
ModrinthPackExportTask::ModrinthPackExportTask(const QString& name,
|
||||||
|
const QString& version,
|
||||||
|
const QString& summary,
|
||||||
|
InstancePtr instance,
|
||||||
|
const QString& output,
|
||||||
|
MMCZip::FilterFunction filter)
|
||||||
|
: name(name)
|
||||||
|
, version(version)
|
||||||
|
, summary(summary)
|
||||||
|
, instance(instance)
|
||||||
|
, mcInstance(dynamic_cast<MinecraftInstance*>(instance.get()))
|
||||||
|
, gameRoot(instance->gameRoot())
|
||||||
|
, output(output)
|
||||||
|
, filter(filter)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void ModrinthPackExportTask::executeTask()
|
||||||
|
{
|
||||||
|
setStatus(tr("Searching for files..."));
|
||||||
|
setProgress(0, 0);
|
||||||
|
collectFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ModrinthPackExportTask::abort()
|
||||||
|
{
|
||||||
|
if (task != nullptr) {
|
||||||
|
task->abort();
|
||||||
|
task = nullptr;
|
||||||
|
emitAborted();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildZipFuture.isRunning()) {
|
||||||
|
buildZipFuture.cancel();
|
||||||
|
// NOTE: Here we don't do `emitAborted()` because it will be done when `buildZipFuture` actually cancels, which may not occur immediately.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModrinthPackExportTask::collectFiles()
|
||||||
|
{
|
||||||
|
setAbortable(false);
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
files.clear();
|
||||||
|
if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) {
|
||||||
|
emitFailed(tr("Could not search for files"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingHashes.clear();
|
||||||
|
resolvedFiles.clear();
|
||||||
|
|
||||||
|
if (mcInstance) {
|
||||||
|
mcInstance->loaderModList()->update();
|
||||||
|
connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &ModrinthPackExportTask::collectHashes);
|
||||||
|
} else
|
||||||
|
collectHashes();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModrinthPackExportTask::collectHashes()
|
||||||
|
{
|
||||||
|
for (const QFileInfo& file : files) {
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
|
||||||
|
// require sensible file types
|
||||||
|
if (!std::any_of(PREFIXES.begin(), PREFIXES.end(),
|
||||||
|
[&relative](const QString& prefix) { return relative.startsWith(prefix + QDir::separator()); }))
|
||||||
|
continue;
|
||||||
|
if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) {
|
||||||
|
return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled");
|
||||||
|
})) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QCryptographicHash sha512(QCryptographicHash::Algorithm::Sha512);
|
||||||
|
|
||||||
|
QFile openFile(file.absoluteFilePath());
|
||||||
|
if (!openFile.open(QFile::ReadOnly)) {
|
||||||
|
qWarning() << "Could not open" << file << "for hashing";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray data = openFile.readAll();
|
||||||
|
if (openFile.error() != QFileDevice::NoError) {
|
||||||
|
qWarning() << "Could not read" << file;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sha512.addData(data);
|
||||||
|
|
||||||
|
auto allMods = mcInstance->loaderModList()->allMods();
|
||||||
|
if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; });
|
||||||
|
modIter != allMods.end()) {
|
||||||
|
const Mod* mod = *modIter;
|
||||||
|
if (mod->metadata() != nullptr) {
|
||||||
|
QUrl& url = mod->metadata()->url;
|
||||||
|
// ensure the url is permitted on modrinth.com
|
||||||
|
if (!url.isEmpty() && BuildConfig.MODRINTH_MRPACK_HOSTS.contains(url.host())) {
|
||||||
|
qDebug() << "Resolving" << relative << "from index";
|
||||||
|
|
||||||
|
QCryptographicHash sha1(QCryptographicHash::Algorithm::Sha1);
|
||||||
|
sha1.addData(data);
|
||||||
|
|
||||||
|
ResolvedFile file{ sha1.result().toHex(), sha512.result().toHex(), url.toString(), openFile.size() };
|
||||||
|
resolvedFiles[relative] = file;
|
||||||
|
|
||||||
|
// nice! we've managed to resolve based on local metadata!
|
||||||
|
// no need to enqueue it
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "Enqueueing" << relative << "for Modrinth query";
|
||||||
|
pendingHashes[relative] = sha512.result().toHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
setAbortable(true);
|
||||||
|
makeApiRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModrinthPackExportTask::makeApiRequest()
|
||||||
|
{
|
||||||
|
if (pendingHashes.isEmpty())
|
||||||
|
buildZip();
|
||||||
|
else {
|
||||||
|
QByteArray* response = new QByteArray;
|
||||||
|
task = api.currentVersions(pendingHashes.values(), "sha512", response);
|
||||||
|
connect(task.get(), &NetJob::succeeded, [this, response]() { parseApiResponse(response); });
|
||||||
|
connect(task.get(), &NetJob::failed, this, &ModrinthPackExportTask::emitFailed);
|
||||||
|
task->start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModrinthPackExportTask::parseApiResponse(const QByteArray* response)
|
||||||
|
{
|
||||||
|
task = nullptr;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const QJsonDocument doc = Json::requireDocument(*response);
|
||||||
|
|
||||||
|
QMapIterator<QString, QString> iterator(pendingHashes);
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
iterator.next();
|
||||||
|
|
||||||
|
const QJsonObject obj = doc[iterator.value()].toObject();
|
||||||
|
if (obj.isEmpty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const QJsonArray files = obj["files"].toArray();
|
||||||
|
if (auto fileIter = std::find_if(files.begin(), files.end(),
|
||||||
|
[&iterator](const QJsonValue& file) { return file["hashes"]["sha512"] == iterator.value(); });
|
||||||
|
fileIter != files.end()) {
|
||||||
|
// map the file to the url
|
||||||
|
resolvedFiles[iterator.key()] =
|
||||||
|
ResolvedFile{ fileIter->toObject()["hashes"].toObject()["sha1"].toString(), iterator.value(),
|
||||||
|
fileIter->toObject()["url"].toString(), fileIter->toObject()["size"].toInt() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const Json::JsonException& e) {
|
||||||
|
emitFailed(tr("Failed to parse versions response: %1").arg(e.what()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingHashes.clear();
|
||||||
|
buildZip();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModrinthPackExportTask::buildZip()
|
||||||
|
{
|
||||||
|
setStatus(tr("Adding files..."));
|
||||||
|
|
||||||
|
buildZipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() {
|
||||||
|
QuaZip zip(output);
|
||||||
|
if (!zip.open(QuaZip::mdCreate)) {
|
||||||
|
QFile::remove(output);
|
||||||
|
return BuildZipResult(tr("Could not create file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildZipFuture.isCanceled())
|
||||||
|
return BuildZipResult();
|
||||||
|
|
||||||
|
QuaZipFile indexFile(&zip);
|
||||||
|
if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo("modrinth.index.json"))) {
|
||||||
|
QFile::remove(output);
|
||||||
|
return BuildZipResult(tr("Could not create index"));
|
||||||
|
}
|
||||||
|
indexFile.write(generateIndex());
|
||||||
|
|
||||||
|
size_t progress = 0;
|
||||||
|
for (const QFileInfo& file : files) {
|
||||||
|
if (buildZipFuture.isCanceled()) {
|
||||||
|
QFile::remove(output);
|
||||||
|
return BuildZipResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
setProgress(progress, files.length());
|
||||||
|
const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
|
||||||
|
if (!resolvedFiles.contains(relative) && !JlCompress::compressFile(&zip, file.absoluteFilePath(), "overrides/" + relative)) {
|
||||||
|
QFile::remove(output);
|
||||||
|
return BuildZipResult(tr("Could not read and compress %1").arg(relative));
|
||||||
|
}
|
||||||
|
progress++;
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.close();
|
||||||
|
|
||||||
|
if (zip.getZipError() != 0) {
|
||||||
|
QFile::remove(output);
|
||||||
|
return BuildZipResult(tr("A zip error occurred"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return BuildZipResult();
|
||||||
|
});
|
||||||
|
connect(&buildZipWatcher, &QFutureWatcher<BuildZipResult>::finished, this, &ModrinthPackExportTask::finish);
|
||||||
|
buildZipWatcher.setFuture(buildZipFuture);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModrinthPackExportTask::finish()
|
||||||
|
{
|
||||||
|
if (buildZipFuture.isCanceled())
|
||||||
|
emitAborted();
|
||||||
|
else {
|
||||||
|
const BuildZipResult result = buildZipFuture.result();
|
||||||
|
if (result.has_value())
|
||||||
|
emitFailed(result.value());
|
||||||
|
else
|
||||||
|
emitSucceeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray ModrinthPackExportTask::generateIndex()
|
||||||
|
{
|
||||||
|
QJsonObject obj;
|
||||||
|
obj["formatVersion"] = 1;
|
||||||
|
obj["game"] = "minecraft";
|
||||||
|
obj["name"] = name;
|
||||||
|
obj["versionId"] = version;
|
||||||
|
if (!summary.isEmpty())
|
||||||
|
obj["summary"] = summary;
|
||||||
|
|
||||||
|
if (mcInstance) {
|
||||||
|
auto profile = mcInstance->getPackProfile();
|
||||||
|
// collect all supported components
|
||||||
|
const ComponentPtr minecraft = profile->getComponent("net.minecraft");
|
||||||
|
const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader");
|
||||||
|
const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader");
|
||||||
|
const ComponentPtr forge = profile->getComponent("net.minecraftforge");
|
||||||
|
|
||||||
|
// convert all available components to mrpack dependencies
|
||||||
|
QJsonObject dependencies;
|
||||||
|
if (minecraft != nullptr)
|
||||||
|
dependencies["minecraft"] = minecraft->m_version;
|
||||||
|
if (quilt != nullptr)
|
||||||
|
dependencies["quilt-loader"] = quilt->m_version;
|
||||||
|
if (fabric != nullptr)
|
||||||
|
dependencies["fabric-loader"] = fabric->m_version;
|
||||||
|
if (forge != nullptr)
|
||||||
|
dependencies["forge"] = forge->m_version;
|
||||||
|
|
||||||
|
obj["dependencies"] = dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray files;
|
||||||
|
QMapIterator<QString, ResolvedFile> iterator(resolvedFiles);
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
iterator.next();
|
||||||
|
|
||||||
|
const ResolvedFile& value = iterator.value();
|
||||||
|
|
||||||
|
QJsonObject file;
|
||||||
|
QString path = iterator.key();
|
||||||
|
path.replace(QDir::separator(), "/");
|
||||||
|
file["path"] = path;
|
||||||
|
file["downloads"] = QJsonArray({ iterator.value().url });
|
||||||
|
|
||||||
|
QJsonObject hashes;
|
||||||
|
hashes["sha1"] = value.sha1;
|
||||||
|
hashes["sha512"] = value.sha512;
|
||||||
|
|
||||||
|
file["hashes"] = hashes;
|
||||||
|
file["fileSize"] = value.size;
|
||||||
|
|
||||||
|
files << file;
|
||||||
|
}
|
||||||
|
obj["files"] = files;
|
||||||
|
|
||||||
|
return QJsonDocument(obj).toJson(QJsonDocument::Compact);
|
||||||
|
}
|
77
launcher/modplatform/modrinth/ModrinthPackExportTask.h
Normal file
77
launcher/modplatform/modrinth/ModrinthPackExportTask.h
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// 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 <QFuture>
|
||||||
|
#include <QFutureWatcher>
|
||||||
|
#include "BaseInstance.h"
|
||||||
|
#include "MMCZip.h"
|
||||||
|
#include "minecraft/MinecraftInstance.h"
|
||||||
|
#include "modplatform/modrinth/ModrinthAPI.h"
|
||||||
|
#include "tasks/Task.h"
|
||||||
|
|
||||||
|
class ModrinthPackExportTask : public Task {
|
||||||
|
public:
|
||||||
|
ModrinthPackExportTask(const QString& name,
|
||||||
|
const QString& version,
|
||||||
|
const QString& summary,
|
||||||
|
InstancePtr instance,
|
||||||
|
const QString& output,
|
||||||
|
MMCZip::FilterFunction filter);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void executeTask() override;
|
||||||
|
bool abort() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct ResolvedFile {
|
||||||
|
QString sha1, sha512, url;
|
||||||
|
qint64 size;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const QStringList PREFIXES;
|
||||||
|
static const QStringList FILE_EXTENSIONS;
|
||||||
|
|
||||||
|
// inputs
|
||||||
|
const QString name, version, summary;
|
||||||
|
const InstancePtr instance;
|
||||||
|
MinecraftInstance* mcInstance;
|
||||||
|
const QDir gameRoot;
|
||||||
|
const QString output;
|
||||||
|
const MMCZip::FilterFunction filter;
|
||||||
|
|
||||||
|
typedef std::optional<QString> BuildZipResult;
|
||||||
|
|
||||||
|
ModrinthAPI api;
|
||||||
|
QFileInfoList files;
|
||||||
|
QMap<QString, QString> pendingHashes;
|
||||||
|
QMap<QString, ResolvedFile> resolvedFiles;
|
||||||
|
Task::Ptr task;
|
||||||
|
QFuture<BuildZipResult> buildZipFuture;
|
||||||
|
QFutureWatcher<BuildZipResult> buildZipWatcher;
|
||||||
|
|
||||||
|
void collectFiles();
|
||||||
|
void collectHashes();
|
||||||
|
void makeApiRequest();
|
||||||
|
void parseApiResponse(const QByteArray* response);
|
||||||
|
void buildZip();
|
||||||
|
void finish();
|
||||||
|
|
||||||
|
QByteArray generateIndex();
|
||||||
|
};
|
@ -2,7 +2,7 @@
|
|||||||
/*
|
/*
|
||||||
* Prism Launcher - Minecraft Launcher
|
* Prism Launcher - Minecraft Launcher
|
||||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||||
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
|
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -107,6 +107,7 @@
|
|||||||
#include "ui/dialogs/CopyInstanceDialog.h"
|
#include "ui/dialogs/CopyInstanceDialog.h"
|
||||||
#include "ui/dialogs/EditAccountDialog.h"
|
#include "ui/dialogs/EditAccountDialog.h"
|
||||||
#include "ui/dialogs/ExportInstanceDialog.h"
|
#include "ui/dialogs/ExportInstanceDialog.h"
|
||||||
|
#include "ui/dialogs/ExportMrPackDialog.h"
|
||||||
#include "ui/dialogs/ImportResourceDialog.h"
|
#include "ui/dialogs/ImportResourceDialog.h"
|
||||||
#include "ui/themes/ITheme.h"
|
#include "ui/themes/ITheme.h"
|
||||||
#include "ui/themes/ThemeManager.h"
|
#include "ui/themes/ThemeManager.h"
|
||||||
@ -397,6 +398,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
|
|||||||
// removing this looks stupid
|
// removing this looks stupid
|
||||||
view->setFocus();
|
view->setFocus();
|
||||||
|
|
||||||
|
ui->actionExportInstance->setMenu(ui->exportInstanceMenu);
|
||||||
|
|
||||||
retranslateUi();
|
retranslateUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1359,7 +1362,7 @@ void MainWindow::on_actionDeleteInstance_triggered()
|
|||||||
APPLICATION->instances()->deleteInstance(id);
|
APPLICATION->instances()->deleteInstance(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::on_actionExportInstance_triggered()
|
void MainWindow::on_actionExportInstanceZip_triggered()
|
||||||
{
|
{
|
||||||
if (m_selectedInstance)
|
if (m_selectedInstance)
|
||||||
{
|
{
|
||||||
@ -1368,6 +1371,15 @@ void MainWindow::on_actionExportInstance_triggered()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_actionExportInstanceMrPack_triggered()
|
||||||
|
{
|
||||||
|
if (m_selectedInstance)
|
||||||
|
{
|
||||||
|
ExportMrPackDialog dlg(m_selectedInstance, this);
|
||||||
|
dlg.exec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::on_actionRenameInstance_triggered()
|
void MainWindow::on_actionRenameInstance_triggered()
|
||||||
{
|
{
|
||||||
if (m_selectedInstance)
|
if (m_selectedInstance)
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
/*
|
/*
|
||||||
* PolyMC - Minecraft Launcher
|
* Prism Launcher - Minecraft Launcher
|
||||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
* 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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -151,7 +152,9 @@ private slots:
|
|||||||
void deleteGroup();
|
void deleteGroup();
|
||||||
void undoTrashInstance();
|
void undoTrashInstance();
|
||||||
|
|
||||||
void on_actionExportInstance_triggered();
|
inline void on_actionExportInstance_triggered() { on_actionExportInstanceZip_triggered(); }
|
||||||
|
void on_actionExportInstanceZip_triggered();
|
||||||
|
void on_actionExportInstanceMrPack_triggered();
|
||||||
|
|
||||||
void on_actionRenameInstance_triggered();
|
void on_actionRenameInstance_triggered();
|
||||||
|
|
||||||
|
@ -150,6 +150,10 @@
|
|||||||
<addaction name="actionChangeInstGroup"/>
|
<addaction name="actionChangeInstGroup"/>
|
||||||
<addaction name="actionViewSelectedInstFolder"/>
|
<addaction name="actionViewSelectedInstFolder"/>
|
||||||
<addaction name="actionExportInstance"/>
|
<addaction name="actionExportInstance"/>
|
||||||
|
<widget class="QMenu" name="exportInstanceMenu">
|
||||||
|
<addaction name="actionExportInstanceZip"/>
|
||||||
|
<addaction name="actionExportInstanceMrPack"/>
|
||||||
|
</widget>
|
||||||
<addaction name="actionCopyInstance"/>
|
<addaction name="actionCopyInstance"/>
|
||||||
<addaction name="actionDeleteInstance"/>
|
<addaction name="actionDeleteInstance"/>
|
||||||
<addaction name="actionCreateInstanceShortcut"/>
|
<addaction name="actionCreateInstanceShortcut"/>
|
||||||
@ -459,10 +463,17 @@
|
|||||||
<string>E&xport...</string>
|
<string>E&xport...</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Export the selected instance as a zip file.</string>
|
<string>Export the selected instance to supported formats.</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="shortcut">
|
</action>
|
||||||
<string>Ctrl+E</string>
|
<action name="actionExportInstanceZip">
|
||||||
|
<property name="text">
|
||||||
|
<string>Prism Launcher (zip)</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionExportInstanceMrPack">
|
||||||
|
<property name="text">
|
||||||
|
<string>Modrinth (mrpack)</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionCreateInstanceShortcut">
|
<action name="actionCreateInstanceShortcut">
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
/*
|
/*
|
||||||
* PolyMC - Minecraft Launcher
|
* Prism Launcher - Minecraft Launcher
|
||||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
* 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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -46,301 +47,21 @@
|
|||||||
#include <QSaveFile>
|
#include <QSaveFile>
|
||||||
#include <QStack>
|
#include <QStack>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
|
||||||
#include "StringUtils.h"
|
|
||||||
#include "SeparatorPrefixTree.h"
|
#include "SeparatorPrefixTree.h"
|
||||||
#include "Application.h"
|
#include "Application.h"
|
||||||
#include <icons/IconList.h>
|
#include <icons/IconList.h>
|
||||||
#include <FileSystem.h>
|
#include <FileSystem.h>
|
||||||
|
|
||||||
class PackIgnoreProxy : public QSortFilterProxyModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
PackIgnoreProxy(InstancePtr instance, QObject *parent) : QSortFilterProxyModel(parent)
|
|
||||||
{
|
|
||||||
m_instance = instance;
|
|
||||||
}
|
|
||||||
// NOTE: Sadly, we have to do sorting ourselves.
|
|
||||||
bool lessThan(const QModelIndex &left, const QModelIndex &right) const
|
|
||||||
{
|
|
||||||
QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
|
|
||||||
if (!fsm)
|
|
||||||
{
|
|
||||||
return QSortFilterProxyModel::lessThan(left, right);
|
|
||||||
}
|
|
||||||
bool asc = sortOrder() == Qt::AscendingOrder ? true : false;
|
|
||||||
|
|
||||||
QFileInfo leftFileInfo = fsm->fileInfo(left);
|
|
||||||
QFileInfo rightFileInfo = fsm->fileInfo(right);
|
|
||||||
|
|
||||||
if (!leftFileInfo.isDir() && rightFileInfo.isDir())
|
|
||||||
{
|
|
||||||
return !asc;
|
|
||||||
}
|
|
||||||
if (leftFileInfo.isDir() && !rightFileInfo.isDir())
|
|
||||||
{
|
|
||||||
return asc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort and proxy model breaks the original model...
|
|
||||||
if (sortColumn() == 0)
|
|
||||||
{
|
|
||||||
return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(),
|
|
||||||
Qt::CaseInsensitive) < 0;
|
|
||||||
}
|
|
||||||
if (sortColumn() == 1)
|
|
||||||
{
|
|
||||||
auto leftSize = leftFileInfo.size();
|
|
||||||
auto rightSize = rightFileInfo.size();
|
|
||||||
if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir()))
|
|
||||||
{
|
|
||||||
return StringUtils::naturalCompare(leftFileInfo.fileName(),
|
|
||||||
rightFileInfo.fileName(),
|
|
||||||
Qt::CaseInsensitive) < 0
|
|
||||||
? asc
|
|
||||||
: !asc;
|
|
||||||
}
|
|
||||||
return leftSize < rightSize;
|
|
||||||
}
|
|
||||||
return QSortFilterProxyModel::lessThan(left, right);
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual Qt::ItemFlags flags(const QModelIndex &index) const
|
|
||||||
{
|
|
||||||
if (!index.isValid())
|
|
||||||
return Qt::NoItemFlags;
|
|
||||||
|
|
||||||
auto sourceIndex = mapToSource(index);
|
|
||||||
Qt::ItemFlags flags = sourceIndex.flags();
|
|
||||||
if (index.column() == 0)
|
|
||||||
{
|
|
||||||
flags |= Qt::ItemIsUserCheckable;
|
|
||||||
if (sourceIndex.model()->hasChildren(sourceIndex))
|
|
||||||
{
|
|
||||||
flags |= Qt::ItemIsAutoTristate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return flags;
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const
|
|
||||||
{
|
|
||||||
QModelIndex sourceIndex = mapToSource(index);
|
|
||||||
|
|
||||||
if (index.column() == 0 && role == Qt::CheckStateRole)
|
|
||||||
{
|
|
||||||
QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
|
|
||||||
auto blockedPath = relPath(fsm->filePath(sourceIndex));
|
|
||||||
auto cover = blocked.cover(blockedPath);
|
|
||||||
if (!cover.isNull())
|
|
||||||
{
|
|
||||||
return QVariant(Qt::Unchecked);
|
|
||||||
}
|
|
||||||
else if (blocked.exists(blockedPath))
|
|
||||||
{
|
|
||||||
return QVariant(Qt::PartiallyChecked);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return QVariant(Qt::Checked);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sourceIndex.data(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual bool setData(const QModelIndex &index, const QVariant &value,
|
|
||||||
int role = Qt::EditRole)
|
|
||||||
{
|
|
||||||
if (index.column() == 0 && role == Qt::CheckStateRole)
|
|
||||||
{
|
|
||||||
Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
|
|
||||||
return setFilterState(index, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
QModelIndex sourceIndex = mapToSource(index);
|
|
||||||
return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, role);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString relPath(const QString &path) const
|
|
||||||
{
|
|
||||||
QString prefix = QDir().absoluteFilePath(m_instance->instanceRoot());
|
|
||||||
prefix += '/';
|
|
||||||
if (!path.startsWith(prefix))
|
|
||||||
{
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
return path.mid(prefix.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool setFilterState(QModelIndex index, Qt::CheckState state)
|
|
||||||
{
|
|
||||||
QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
|
|
||||||
|
|
||||||
if (!fsm)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QModelIndex sourceIndex = mapToSource(index);
|
|
||||||
auto blockedPath = relPath(fsm->filePath(sourceIndex));
|
|
||||||
bool changed = false;
|
|
||||||
if (state == Qt::Unchecked)
|
|
||||||
{
|
|
||||||
// blocking a path
|
|
||||||
auto &node = blocked.insert(blockedPath);
|
|
||||||
// get rid of all blocked nodes below
|
|
||||||
node.clear();
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
else if (state == Qt::Checked || state == Qt::PartiallyChecked)
|
|
||||||
{
|
|
||||||
if (!blocked.remove(blockedPath))
|
|
||||||
{
|
|
||||||
auto cover = blocked.cover(blockedPath);
|
|
||||||
qDebug() << "Blocked by cover" << cover;
|
|
||||||
// uncover
|
|
||||||
blocked.remove(cover);
|
|
||||||
// block all contents, except for any cover
|
|
||||||
QModelIndex rootIndex =
|
|
||||||
fsm->index(FS::PathCombine(m_instance->instanceRoot(), cover));
|
|
||||||
QModelIndex doing = rootIndex;
|
|
||||||
int row = 0;
|
|
||||||
QStack<QModelIndex> todo;
|
|
||||||
while (1)
|
|
||||||
{
|
|
||||||
auto node = fsm->index(row, 0, doing);
|
|
||||||
if (!node.isValid())
|
|
||||||
{
|
|
||||||
if (!todo.size())
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
doing = todo.pop();
|
|
||||||
row = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auto relpath = relPath(fsm->filePath(node));
|
|
||||||
if (blockedPath.startsWith(relpath)) // cover found?
|
|
||||||
{
|
|
||||||
// continue processing cover later
|
|
||||||
todo.push(node);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// or just block this one.
|
|
||||||
blocked.insert(relpath);
|
|
||||||
}
|
|
||||||
row++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (changed)
|
|
||||||
{
|
|
||||||
// update the thing
|
|
||||||
emit dataChanged(index, index, {Qt::CheckStateRole});
|
|
||||||
// update everything above index
|
|
||||||
QModelIndex up = index.parent();
|
|
||||||
while (1)
|
|
||||||
{
|
|
||||||
if (!up.isValid())
|
|
||||||
break;
|
|
||||||
emit dataChanged(up, up, {Qt::CheckStateRole});
|
|
||||||
up = up.parent();
|
|
||||||
}
|
|
||||||
// and everything below the index
|
|
||||||
QModelIndex doing = index;
|
|
||||||
int row = 0;
|
|
||||||
QStack<QModelIndex> todo;
|
|
||||||
while (1)
|
|
||||||
{
|
|
||||||
auto node = this->index(row, 0, doing);
|
|
||||||
if (!node.isValid())
|
|
||||||
{
|
|
||||||
if (!todo.size())
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
doing = todo.pop();
|
|
||||||
row = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emit dataChanged(node, node, {Qt::CheckStateRole});
|
|
||||||
todo.push(node);
|
|
||||||
row++;
|
|
||||||
}
|
|
||||||
// siblings and unrelated nodes are ignored
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool shouldExpand(QModelIndex index)
|
|
||||||
{
|
|
||||||
QModelIndex sourceIndex = mapToSource(index);
|
|
||||||
QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
|
|
||||||
if (!fsm)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
auto blockedPath = relPath(fsm->filePath(sourceIndex));
|
|
||||||
auto found = blocked.find(blockedPath);
|
|
||||||
if(found)
|
|
||||||
{
|
|
||||||
return !found->leaf();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setBlockedPaths(QStringList paths)
|
|
||||||
{
|
|
||||||
beginResetModel();
|
|
||||||
blocked.clear();
|
|
||||||
blocked.insert(paths);
|
|
||||||
endResetModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
const SeparatorPrefixTree<'/'> & blockedPaths() const
|
|
||||||
{
|
|
||||||
return blocked;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(source_parent)
|
|
||||||
|
|
||||||
// adjust the columns you want to filter out here
|
|
||||||
// return false for those that will be hidden
|
|
||||||
if (source_column == 2 || source_column == 3)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
InstancePtr m_instance;
|
|
||||||
SeparatorPrefixTree<'/'> blocked;
|
|
||||||
};
|
|
||||||
|
|
||||||
ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent)
|
ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent)
|
||||||
: QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance)
|
: QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance)
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
auto model = new QFileSystemModel(this);
|
auto model = new QFileSystemModel(this);
|
||||||
proxyModel = new PackIgnoreProxy(m_instance, this);
|
model->setIconProvider(&icons);
|
||||||
|
auto root = instance->instanceRoot();
|
||||||
|
proxyModel = new FileIgnoreProxy(root, this);
|
||||||
loadPackIgnore();
|
loadPackIgnore();
|
||||||
proxyModel->setSourceModel(model);
|
proxyModel->setSourceModel(model);
|
||||||
auto root = instance->instanceRoot();
|
|
||||||
ui->treeView->setModel(proxyModel);
|
ui->treeView->setModel(proxyModel);
|
||||||
ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root)));
|
ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root)));
|
||||||
ui->treeView->sortByColumn(0, Qt::AscendingOrder);
|
ui->treeView->sortByColumn(0, Qt::AscendingOrder);
|
||||||
@ -404,22 +125,11 @@ bool ExportInstanceDialog::doExport()
|
|||||||
|
|
||||||
const QString output = QFileDialog::getSaveFileName(
|
const QString output = QFileDialog::getSaveFileName(
|
||||||
this, tr("Export %1").arg(m_instance->name()),
|
this, tr("Export %1").arg(m_instance->name()),
|
||||||
FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr, QFileDialog::DontConfirmOverwrite);
|
FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr);
|
||||||
if (output.isEmpty())
|
if (output.isEmpty())
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (QFile::exists(output))
|
|
||||||
{
|
|
||||||
int ret =
|
|
||||||
QMessageBox::question(this, tr("Overwrite?"),
|
|
||||||
tr("This file already exists. Do you want to overwrite it?"),
|
|
||||||
QMessageBox::No, QMessageBox::Yes);
|
|
||||||
if (ret == QMessageBox::No)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SaveIcon(m_instance);
|
SaveIcon(m_instance);
|
||||||
|
|
||||||
@ -511,5 +221,3 @@ void ExportInstanceDialog::savePackIgnore()
|
|||||||
qWarning() << e.cause();
|
qWarning() << e.cause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#include "ExportInstanceDialog.moc"
|
|
||||||
|
@ -1,16 +1,36 @@
|
|||||||
/* Copyright 2013-2021 MultiMC Contributors
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* Prism Launcher - Minecraft Launcher
|
||||||
|
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* you may not use this file except in compliance with the License.
|
* it under the terms of the GNU General Public License as published by
|
||||||
* You may obtain a copy of the License at
|
* the Free Software Foundation, version 3.
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* 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.
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* You should have received a copy of the GNU General Public License
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
*
|
||||||
* See the License for the specific language governing permissions and
|
* This file incorporates work covered by the following copyright and
|
||||||
* limitations under the License.
|
* 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
|
#pragma once
|
||||||
@ -18,9 +38,10 @@
|
|||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QModelIndex>
|
#include <QModelIndex>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include "FileIgnoreProxy.h"
|
||||||
|
#include "FastFileIconProvider.h"
|
||||||
|
|
||||||
class BaseInstance;
|
class BaseInstance;
|
||||||
class PackIgnoreProxy;
|
|
||||||
typedef std::shared_ptr<BaseInstance> InstancePtr;
|
typedef std::shared_ptr<BaseInstance> InstancePtr;
|
||||||
|
|
||||||
namespace Ui
|
namespace Ui
|
||||||
@ -47,7 +68,8 @@ private:
|
|||||||
private:
|
private:
|
||||||
Ui::ExportInstanceDialog *ui;
|
Ui::ExportInstanceDialog *ui;
|
||||||
InstancePtr m_instance;
|
InstancePtr m_instance;
|
||||||
PackIgnoreProxy * proxyModel;
|
FileIgnoreProxy * proxyModel;
|
||||||
|
FastFileIconProvider icons;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void rowsInserted(QModelIndex parent, int top, int bottom);
|
void rowsInserted(QModelIndex parent, int top, int bottom);
|
||||||
|
109
launcher/ui/dialogs/ExportMrPackDialog.cpp
Normal file
109
launcher/ui/dialogs/ExportMrPackDialog.cpp
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
// 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 "ExportMrPackDialog.h"
|
||||||
|
#include "minecraft/mod/ModFolderModel.h"
|
||||||
|
#include "ui/dialogs/CustomMessageBox.h"
|
||||||
|
#include "ui/dialogs/ProgressDialog.h"
|
||||||
|
#include "ui_ExportMrPackDialog.h"
|
||||||
|
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QFileSystemModel>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include "FastFileIconProvider.h"
|
||||||
|
#include "FileSystem.h"
|
||||||
|
#include "MMCZip.h"
|
||||||
|
#include "modplatform/modrinth/ModrinthPackExportTask.h"
|
||||||
|
|
||||||
|
ExportMrPackDialog::ExportMrPackDialog(InstancePtr instance, QWidget* parent)
|
||||||
|
: QDialog(parent), instance(instance), ui(new Ui::ExportMrPackDialog)
|
||||||
|
{
|
||||||
|
ui->setupUi(this);
|
||||||
|
ui->name->setText(instance->name());
|
||||||
|
ui->summary->setText(instance->notes().split(QRegularExpression("\\r?\\n"))[0]);
|
||||||
|
|
||||||
|
QFileSystemModel* model = new QFileSystemModel(this);
|
||||||
|
model->setIconProvider(&icons);
|
||||||
|
|
||||||
|
// use the game root - everything outside cannot be exported
|
||||||
|
const QDir root(instance->gameRoot());
|
||||||
|
proxy = new FileIgnoreProxy(instance->gameRoot(), this);
|
||||||
|
proxy->setSourceModel(model);
|
||||||
|
|
||||||
|
const QDir::Filters filter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden);
|
||||||
|
|
||||||
|
for (const QString& file : root.entryList(filter)) {
|
||||||
|
if (!(file == "mods" || file == "coremods" || file == "datapacks" || file == "config" || file == "options.txt" ||
|
||||||
|
file == "servers.dat"))
|
||||||
|
proxy->blockedPaths().insert(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get());
|
||||||
|
if (mcInstance) {
|
||||||
|
const QDir index = mcInstance->loaderModList()->indexDir();
|
||||||
|
if (index.exists())
|
||||||
|
proxy->blockedPaths().insert(root.relativeFilePath(index.absolutePath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ui->treeView->setModel(proxy);
|
||||||
|
ui->treeView->setRootIndex(proxy->mapFromSource(model->index(instance->gameRoot())));
|
||||||
|
ui->treeView->sortByColumn(0, Qt::AscendingOrder);
|
||||||
|
|
||||||
|
model->setFilter(filter);
|
||||||
|
model->setRootPath(instance->gameRoot());
|
||||||
|
|
||||||
|
QHeaderView* headerView = ui->treeView->header();
|
||||||
|
headerView->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||||
|
headerView->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExportMrPackDialog::~ExportMrPackDialog()
|
||||||
|
{
|
||||||
|
delete ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExportMrPackDialog::done(int result)
|
||||||
|
{
|
||||||
|
if (result == Accepted) {
|
||||||
|
const QString filename = FS::RemoveInvalidFilenameChars(ui->name->text());
|
||||||
|
const QString output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()),
|
||||||
|
FS::PathCombine(QDir::homePath(), filename + ".mrpack"),
|
||||||
|
"Modrinth pack (*.mrpack *.zip)", nullptr);
|
||||||
|
|
||||||
|
if (output.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
ModrinthPackExportTask task(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output,
|
||||||
|
[this](const QString& path) { return proxy->blockedPaths().covers(path); });
|
||||||
|
|
||||||
|
connect(&task, &Task::failed,
|
||||||
|
[this](const QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
|
||||||
|
connect(&task, &Task::aborted, [this] {
|
||||||
|
CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)
|
||||||
|
->show();
|
||||||
|
});
|
||||||
|
|
||||||
|
ProgressDialog progress(this);
|
||||||
|
progress.setSkipButton(true, tr("Abort"));
|
||||||
|
if (progress.execWithTask(&task) != QDialog::Accepted)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDialog::done(result);
|
||||||
|
}
|
44
launcher/ui/dialogs/ExportMrPackDialog.h
Normal file
44
launcher/ui/dialogs/ExportMrPackDialog.h
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// 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 <QDialog>
|
||||||
|
#include "BaseInstance.h"
|
||||||
|
#include "FastFileIconProvider.h"
|
||||||
|
#include "FileIgnoreProxy.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class ExportMrPackDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExportMrPackDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ExportMrPackDialog(InstancePtr instance, QWidget* parent = nullptr);
|
||||||
|
~ExportMrPackDialog();
|
||||||
|
|
||||||
|
void done(int result) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const InstancePtr instance;
|
||||||
|
Ui::ExportMrPackDialog* ui;
|
||||||
|
FileIgnoreProxy* proxy;
|
||||||
|
FastFileIconProvider icons;
|
||||||
|
};
|
132
launcher/ui/dialogs/ExportMrPackDialog.ui
Normal file
132
launcher/ui/dialogs/ExportMrPackDialog.ui
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>ExportMrPackDialog</class>
|
||||||
|
<widget class="QDialog" name="ExportMrPackDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>650</width>
|
||||||
|
<height>413</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Export Modrinth Pack</string>
|
||||||
|
</property>
|
||||||
|
<property name="sizeGripEnabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="information">
|
||||||
|
<property name="title">
|
||||||
|
<string>Information</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QLabel" name="versionLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Summary</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="QLineEdit" name="summary"/>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="nameLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Name</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="summaryLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Version</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QLineEdit" name="name"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QLineEdit" name="version"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="filesLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Files</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QTreeView" name="treeView">
|
||||||
|
<property name="alternatingRowColors">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="selectionMode">
|
||||||
|
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sortingEnabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<attribute name="headerStretchLastSection">
|
||||||
|
<bool>false</bool>
|
||||||
|
</attribute>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<tabstops>
|
||||||
|
<tabstop>name</tabstop>
|
||||||
|
<tabstop>version</tabstop>
|
||||||
|
<tabstop>summary</tabstop>
|
||||||
|
<tabstop>treeView</tabstop>
|
||||||
|
</tabstops>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>ExportMrPackDialog</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>324</x>
|
||||||
|
<y>390</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>324</x>
|
||||||
|
<y>206</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>ExportMrPackDialog</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>324</x>
|
||||||
|
<y>390</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>324</x>
|
||||||
|
<y>206</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
Loading…
Reference in New Issue
Block a user