aboutsummaryrefslogtreecommitdiff
path: root/launcher
diff options
context:
space:
mode:
Diffstat (limited to 'launcher')
-rw-r--r--launcher/Application.cpp41
-rw-r--r--launcher/CMakeLists.txt9
-rw-r--r--launcher/FastFileIconProvider.cpp47
-rw-r--r--launcher/FastFileIconProvider.h26
-rw-r--r--launcher/FileIgnoreProxy.cpp256
-rw-r--r--launcher/FileIgnoreProxy.h72
-rw-r--r--launcher/FileSystem.cpp6
-rw-r--r--launcher/InstanceCopyTask.cpp30
-rw-r--r--launcher/InstanceTask.cpp5
-rw-r--r--launcher/ResourceDownloadTask.cpp49
-rw-r--r--launcher/ResourceDownloadTask.h58
-rw-r--r--launcher/VersionProxyModel.cpp29
-rw-r--r--launcher/VersionProxyModel.h3
-rw-r--r--launcher/java/JavaChecker.cpp4
-rw-r--r--launcher/java/JavaInstallList.cpp5
-rw-r--r--launcher/meta/JsonFormat.cpp7
-rw-r--r--launcher/meta/Version.cpp4
-rw-r--r--launcher/meta/Version.h4
-rw-r--r--launcher/meta/VersionList.cpp4
-rw-r--r--launcher/minecraft/Component.cpp4
-rw-r--r--launcher/minecraft/MinecraftInstance.cpp3
-rw-r--r--launcher/minecraft/OneSixVersionFormat.cpp10
-rw-r--r--launcher/minecraft/VersionFile.h2
-rw-r--r--launcher/minecraft/auth/AccountList.cpp6
-rw-r--r--launcher/minecraft/mod/DataPack.cpp4
-rw-r--r--launcher/minecraft/mod/ResourcePack.cpp3
-rw-r--r--launcher/modplatform/ModIndex.h35
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.cpp2
-rw-r--r--launcher/modplatform/flame/FlameCheckUpdate.cpp22
-rw-r--r--launcher/modplatform/flame/FlameInstanceCreationTask.cpp3
-rw-r--r--launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp28
-rw-r--r--launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp2
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackExportTask.cpp319
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackExportTask.h77
-rw-r--r--launcher/net/ByteArraySink.h15
-rw-r--r--launcher/settings/INIFile.cpp109
-rw-r--r--launcher/tasks/ConcurrentTask.cpp3
-rw-r--r--launcher/translations/TranslationsModel.cpp7
-rw-r--r--launcher/ui/MainWindow.cpp76
-rw-r--r--launcher/ui/MainWindow.h9
-rw-r--r--launcher/ui/MainWindow.ui32
-rw-r--r--launcher/ui/dialogs/ExportInstanceDialog.cpp304
-rw-r--r--launcher/ui/dialogs/ExportInstanceDialog.h46
-rw-r--r--launcher/ui/dialogs/ExportMrPackDialog.cpp124
-rw-r--r--launcher/ui/dialogs/ExportMrPackDialog.h45
-rw-r--r--launcher/ui/dialogs/ExportMrPackDialog.ui136
-rw-r--r--launcher/ui/dialogs/ResourceDownloadDialog.cpp88
-rw-r--r--launcher/ui/dialogs/ResourceDownloadDialog.h17
-rw-r--r--launcher/ui/dialogs/VersionSelectDialog.cpp47
-rw-r--r--launcher/ui/pages/BasePage.h24
-rw-r--r--launcher/ui/pages/global/APIPage.cpp2
-rw-r--r--launcher/ui/pages/global/LauncherPage.ui8
-rw-r--r--launcher/ui/pages/global/MinecraftPage.ui2
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.cpp54
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.h1
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.cpp66
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.h5
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.ui17
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.cpp27
-rw-r--r--launcher/ui/pages/modplatform/ModModel.cpp16
-rw-r--r--launcher/ui/pages/modplatform/ModModel.h5
-rw-r--r--launcher/ui/pages/modplatform/ModPage.cpp43
-rw-r--r--launcher/ui/pages/modplatform/ModPage.h15
-rw-r--r--launcher/ui/pages/modplatform/ResourceModel.cpp68
-rw-r--r--launcher/ui/pages/modplatform/ResourceModel.h14
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.cpp105
-rw-r--r--launcher/ui/pages/modplatform/ResourcePage.h18
-rw-r--r--launcher/ui/pages/modplatform/ShaderPackPage.cpp16
-rw-r--r--launcher/ui/pages/modplatform/ShaderPackPage.h4
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp2
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModel.cpp2
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp2
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameResourceModels.h2
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp2
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp2
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h2
-rw-r--r--launcher/ui/themes/SystemTheme.cpp2
-rw-r--r--launcher/ui/widgets/JavaSettingsWidget.cpp2
-rw-r--r--launcher/ui/widgets/PageContainer.cpp9
-rw-r--r--launcher/ui/widgets/PageContainer.h1
-rw-r--r--launcher/ui/widgets/ProjectItem.cpp11
-rw-r--r--launcher/ui/widgets/ProjectItem.h3
-rw-r--r--launcher/ui/widgets/VersionListView.cpp8
-rw-r--r--launcher/ui/widgets/VersionSelectWidget.cpp46
-rw-r--r--launcher/ui/widgets/VersionSelectWidget.h48
85 files changed, 2097 insertions, 794 deletions
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index 1659eb44..724e6e44 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -376,33 +376,33 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// init the logger
{
- static const QString logBase = BuildConfig.LAUNCHER_NAME + "-%0.log";
- auto moveFile = [](const QString &oldName, const QString &newName)
- {
+ static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log";
+ static const QString logBase = FS::PathCombine("logs", baseLogFile);
+ auto moveFile = [](const QString& oldName, const QString& newName) {
QFile::remove(newName);
QFile::copy(oldName, newName);
QFile::remove(oldName);
};
+ if (FS::ensureFolderPathExists("logs")) { // if this did not fail
+ for (auto i = 0; i <= 4; i++)
+ if (auto oldName = baseLogFile.arg(i);
+ QFile::exists(oldName)) // do not pointlessly delete new files if the old ones are not there
+ moveFile(oldName, logBase.arg(i));
+ }
- moveFile(logBase.arg(3), logBase.arg(4));
- moveFile(logBase.arg(2), logBase.arg(3));
- moveFile(logBase.arg(1), logBase.arg(2));
- moveFile(logBase.arg(0), logBase.arg(1));
+ for (auto i = 4; i > 0; i--)
+ moveFile(logBase.arg(i - 1), logBase.arg(i));
logFile = std::unique_ptr<QFile>(new QFile(logBase.arg(0)));
- if(!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate))
- {
- showFatalErrorMessage(
- "The launcher data folder is not writable!",
- QString(
- "The launcher couldn't create a log file - the data folder is not writable.\n"
- "\n"
- "Make sure you have write permissions to the data folder.\n"
- "(%1)\n"
- "\n"
- "The launcher cannot continue until you fix this problem."
- ).arg(dataPath)
- );
+ if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
+ showFatalErrorMessage("The launcher data folder is not writable!",
+ QString("The launcher couldn't create a log file - the data folder is not writable.\n"
+ "\n"
+ "Make sure you have write permissions to the data folder.\n"
+ "(%1)\n"
+ "\n"
+ "The launcher cannot continue until you fix this problem.")
+ .arg(dataPath));
return;
}
qInstallMessageHandler(appDebugOutput);
@@ -1699,6 +1699,7 @@ bool Application::handleDataMigration(const QString& currentData,
matcher->add(std::make_shared<SimplePrefixMatcher>(configFile));
matcher->add(std::make_shared<SimplePrefixMatcher>(
BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before
+ matcher->add(std::make_shared<SimplePrefixMatcher>("logs/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("accounts.json"));
matcher->add(std::make_shared<SimplePrefixMatcher>("accounts/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("assets/"));
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 273b5449..ce2771a4 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -525,6 +525,8 @@ set(MODRINTH_SOURCES
modplatform/modrinth/ModrinthCheckUpdate.h
modplatform/modrinth/ModrinthInstanceCreationTask.cpp
modplatform/modrinth/ModrinthInstanceCreationTask.h
+ modplatform/modrinth/ModrinthPackExportTask.cpp
+ modplatform/modrinth/ModrinthPackExportTask.h
)
set(PACKWIZ_SOURCES
@@ -720,6 +722,10 @@ SET(LAUNCHER_SOURCES
# FIXME: maybe find a better home for this.
SkinUtils.cpp
SkinUtils.h
+ FileIgnoreProxy.cpp
+ FileIgnoreProxy.h
+ FastFileIconProvider.cpp
+ FastFileIconProvider.h
# GUI - setup wizard
ui/setupwizard/SetupWizard.h
@@ -900,6 +906,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/EditAccountDialog.h
ui/dialogs/ExportInstanceDialog.cpp
ui/dialogs/ExportInstanceDialog.h
+ ui/dialogs/ExportMrPackDialog.cpp
+ ui/dialogs/ExportMrPackDialog.h
ui/dialogs/IconPickerDialog.cpp
ui/dialogs/IconPickerDialog.h
ui/dialogs/ImportResourceDialog.cpp
@@ -1046,6 +1054,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/ProfileSelectDialog.ui
ui/dialogs/SkinUploadDialog.ui
ui/dialogs/ExportInstanceDialog.ui
+ ui/dialogs/ExportMrPackDialog.ui
ui/dialogs/IconPickerDialog.ui
ui/dialogs/ImportResourceDialog.ui
ui/dialogs/MSALoginDialog.ui
diff --git a/launcher/FastFileIconProvider.cpp b/launcher/FastFileIconProvider.cpp
new file mode 100644
index 00000000..f2b6f442
--- /dev/null
+++ b/launcher/FastFileIconProvider.cpp
@@ -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);
+} \ No newline at end of file
diff --git a/launcher/FastFileIconProvider.h b/launcher/FastFileIconProvider.h
new file mode 100644
index 00000000..20853404
--- /dev/null
+++ b/launcher/FastFileIconProvider.h
@@ -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;
+}; \ No newline at end of file
diff --git a/launcher/FileIgnoreProxy.cpp b/launcher/FileIgnoreProxy.cpp
new file mode 100644
index 00000000..a3b7d505
--- /dev/null
+++ b/launcher/FileIgnoreProxy.cpp
@@ -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;
+}
diff --git a/launcher/FileIgnoreProxy.h b/launcher/FileIgnoreProxy.h
new file mode 100644
index 00000000..a5a1153d
--- /dev/null
+++ b/launcher/FileIgnoreProxy.h
@@ -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;
+};
diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp
index d98526df..835ad925 100644
--- a/launcher/FileSystem.cpp
+++ b/launcher/FileSystem.cpp
@@ -102,7 +102,7 @@ namespace fs = ghc::filesystem;
#include <linux/fs.h>
#include <sys/ioctl.h>
#include <unistd.h>
-#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD)
#include <sys/attr.h>
#include <sys/clonefile.h>
#elif defined(Q_OS_WIN)
@@ -1151,7 +1151,7 @@ bool clone_file(const QString& src, const QString& dst, std::error_code& ec)
return false;
}
-#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD)
if (!macos_bsd_clonefile(src_path, dst_path, ec)) {
qDebug() << "failed macos_bsd_clonefile:";
@@ -1380,7 +1380,7 @@ bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std
return true;
}
-#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+#elif defined(Q_OS_MACOS) || defined(Q_OS_OPENBSD)
bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec)
{
diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp
index 4ac3b51a..57a3143a 100644
--- a/launcher/InstanceCopyTask.cpp
+++ b/launcher/InstanceCopyTask.cpp
@@ -39,7 +39,16 @@ void InstanceCopyTask::executeTask()
setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
auto copySaves = [&]() {
- FS::copy savesCopy(FS::PathCombine(m_origInstance->instanceRoot(), "saves"), FS::PathCombine(m_stagingPath, "saves"));
+ QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft"));
+ QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft"));
+
+ QString staging_mc_dir;
+ if (mcDir.exists() && !dotMCDir.exists())
+ staging_mc_dir = mcDir.filePath();
+ else
+ staging_mc_dir = dotMCDir.filePath();
+
+ FS::copy savesCopy(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves"));
savesCopy.followSymlinks(true);
return savesCopy();
@@ -123,6 +132,7 @@ void InstanceCopyTask::copyFinished()
emitFailed(tr("Instance folder copy failed."));
return;
}
+
// FIXME: shouldn't this be able to report errors?
auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg"));
@@ -134,6 +144,24 @@ void InstanceCopyTask::copyFinished()
}
if (m_useLinks)
inst->addLinkedInstanceId(m_origInstance->id());
+ if (m_useLinks) {
+ auto allowed_symlinks_file = QFileInfo(FS::PathCombine(inst->gameRoot(), "allowed_symlinks.txt"));
+
+ QByteArray allowed_symlinks;
+ if (allowed_symlinks_file.exists()) {
+ allowed_symlinks.append(FS::read(allowed_symlinks_file.filePath()));
+ if (allowed_symlinks.right(1) != "\n")
+ allowed_symlinks.append("\n"); // we want to be on a new line
+ }
+ allowed_symlinks.append(m_origInstance->gameRoot().toUtf8());
+ allowed_symlinks.append("\n");
+ if (allowed_symlinks_file.isSymLink())
+ FS::deletePath(allowed_symlinks_file
+ .filePath()); // we dont want to modify the original. also make sure the resulting file is not itself a link.
+
+ FS::write(allowed_symlinks_file.filePath(), allowed_symlinks);
+ }
+
emitSucceeded();
}
diff --git a/launcher/InstanceTask.cpp b/launcher/InstanceTask.cpp
index 06682782..b16a40ba 100644
--- a/launcher/InstanceTask.cpp
+++ b/launcher/InstanceTask.cpp
@@ -45,7 +45,10 @@ QString InstanceName::name() const
{
if (!m_modified_name.isEmpty())
return modifiedName();
- return QString("%1 %2").arg(m_original_name, m_original_version);
+ if (!m_original_version.isEmpty())
+ return QString("%1 %2").arg(m_original_name, m_original_version);
+
+ return m_original_name;
}
QString InstanceName::originalName() const
diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp
index 61b918aa..06c03c77 100644
--- a/launcher/ResourceDownloadTask.cpp
+++ b/launcher/ResourceDownloadTask.cpp
@@ -1,21 +1,21 @@
// SPDX-License-Identifier: GPL-3.0-only
-/*
-* Prism Launcher - Minecraft Launcher
-* Copyright (c) 2022-2023 flowln <flowlnlnln@gmail.com>
-* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
-*
-* 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/>.
-*/
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022-2023 flowln <flowlnlnln@gmail.com>
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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 "ResourceDownloadTask.h"
@@ -24,14 +24,15 @@
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/ResourceFolderModel.h"
-ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack pack,
+ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
ModPlatform::IndexedVersion version,
const std::shared_ptr<ResourceFolderModel> packs,
- bool is_indexed)
- : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs)
+ bool is_indexed,
+ QString custom_target_folder)
+ : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs), m_custom_target_folder(custom_target_folder)
{
if (auto model = dynamic_cast<ModFolderModel*>(m_pack_model.get()); model && is_indexed) {
- m_update_task.reset(new LocalModUpdateTask(model->indexDir(), m_pack, m_pack_version));
+ m_update_task.reset(new LocalModUpdateTask(model->indexDir(), *m_pack, m_pack_version));
connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ResourceDownloadTask::hasOldResource);
addTask(m_update_task);
@@ -40,13 +41,13 @@ ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack pack,
m_filesNetJob.reset(new NetJob(tr("Resource download"), APPLICATION->network()));
m_filesNetJob->setStatus(tr("Downloading resource:\n%1").arg(m_pack_version.downloadUrl));
- QDir dir { m_pack_model->dir() };
+ QDir dir{ m_pack_model->dir() };
{
// FIXME: Make this more generic. May require adding additional info to IndexedVersion,
// or adquiring a reference to the base instance.
- if (!m_pack_version.custom_target_folder.isEmpty()) {
+ if (!m_custom_target_folder.isEmpty()) {
dir.cdUp();
- dir.cd(m_pack_version.custom_target_folder);
+ dir.cd(m_custom_target_folder);
}
}
diff --git a/launcher/ResourceDownloadTask.h b/launcher/ResourceDownloadTask.h
index 73ad2d07..09147c8c 100644
--- a/launcher/ResourceDownloadTask.h
+++ b/launcher/ResourceDownloadTask.h
@@ -1,44 +1,51 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
-* Prism Launcher - Minecraft Launcher
-* Copyright (c) 2022-2023 flowln <flowlnlnln@gmail.com>
-* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
-*
-* 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/>.
-*/
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022-2023 flowln <flowlnlnln@gmail.com>
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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 "net/NetJob.h"
#include "tasks/SequentialTask.h"
-#include "modplatform/ModIndex.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
+#include "modplatform/ModIndex.h"
class ResourceFolderModel;
class ResourceDownloadTask : public SequentialTask {
Q_OBJECT
-public:
- explicit ResourceDownloadTask(ModPlatform::IndexedPack pack, ModPlatform::IndexedVersion version, const std::shared_ptr<ResourceFolderModel> packs, bool is_indexed = true);
+ public:
+ explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
+ ModPlatform::IndexedVersion version,
+ const std::shared_ptr<ResourceFolderModel> packs,
+ bool is_indexed = true,
+ QString custom_target_folder = {});
const QString& getFilename() const { return m_pack_version.fileName; }
- const QString& getCustomPath() const { return m_pack_version.custom_target_folder; }
+ const QString& getCustomPath() const { return m_custom_target_folder; }
const QVariant& getVersionID() const { return m_pack_version.fileId; }
+ const QString& getName() const { return m_pack->name; }
+ ModPlatform::IndexedPack::Ptr getPack() { return m_pack; }
-private:
- ModPlatform::IndexedPack m_pack;
+ private:
+ ModPlatform::IndexedPack::Ptr m_pack;
ModPlatform::IndexedVersion m_pack_version;
const std::shared_ptr<ResourceFolderModel> m_pack_model;
+ QString m_custom_target_folder;
NetJob::Ptr m_filesNetJob;
LocalModUpdateTask::Ptr m_update_task;
@@ -47,11 +54,8 @@ private:
void downloadFailed(QString reason);
void downloadSucceeded();
- std::tuple<QString, QString> to_delete {"", ""};
+ std::tuple<QString, QString> to_delete{ "", "" };
-private slots:
+ private slots:
void hasOldResource(QString name, QString filename);
};
-
-
-
diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp
index 6aba268d..e5c66566 100644
--- a/launcher/VersionProxyModel.cpp
+++ b/launcher/VersionProxyModel.cpp
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * 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
@@ -54,9 +55,14 @@ public:
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
const auto &filters = m_parent->filters();
+ const QString &search = m_parent->search();
+ const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
+
+ if (!search.isEmpty() && !sourceModel()->data(idx, BaseVersionList::VersionRole).toString().contains(search, Qt::CaseInsensitive))
+ return false;
+
for (auto it = filters.begin(); it != filters.end(); ++it)
{
- auto idx = sourceModel()->index(source_row, 0, source_parent);
auto data = sourceModel()->data(idx, it.key());
auto match = data.toString();
if(!it.value()->accepts(match))
@@ -206,10 +212,6 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const
return tr("Latest");
}
}
- else if(index.row() == 0)
- {
- return tr("Latest");
- }
}
}
default:
@@ -239,10 +241,6 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const
return APPLICATION->getThemedIcon("bug");
}
}
- else if(index.row() == 0)
- {
- return APPLICATION->getThemedIcon("bug");
- }
QPixmap pixmap;
QPixmapCache::find("placeholder", &pixmap);
if(!pixmap)
@@ -431,6 +429,7 @@ QModelIndex VersionProxyModel::getVersion(const QString& version) const
void VersionProxyModel::clearFilters()
{
m_filters.clear();
+ m_search.clear();
filterModel->filterChanged();
}
@@ -440,11 +439,21 @@ void VersionProxyModel::setFilter(const BaseVersionList::ModelRoles column, Filt
filterModel->filterChanged();
}
+void VersionProxyModel::setSearch(const QString &search) {
+ m_search = search;
+ filterModel->filterChanged();
+}
+
const VersionProxyModel::FilterMap &VersionProxyModel::filters() const
{
return m_filters;
}
+const QString &VersionProxyModel::search() const
+{
+ return m_search;
+}
+
void VersionProxyModel::sourceAboutToBeReset()
{
beginResetModel();
diff --git a/launcher/VersionProxyModel.h b/launcher/VersionProxyModel.h
index 8991c31b..6434376c 100644
--- a/launcher/VersionProxyModel.h
+++ b/launcher/VersionProxyModel.h
@@ -38,7 +38,9 @@ public:
virtual void setSourceModel(QAbstractItemModel *sourceModel) override;
const FilterMap &filters() const;
+ const QString &search() const;
void setFilter(const BaseVersionList::ModelRoles column, Filter * filter);
+ void setSearch(const QString &search);
void clearFilters();
QModelIndex getRecommended() const;
QModelIndex getVersion(const QString & version) const;
@@ -59,6 +61,7 @@ private slots:
private:
QList<Column> m_columns;
FilterMap m_filters;
+ QString m_search;
BaseVersionList::RoleList roles;
VersionFilterModel * filterModel;
bool hasRecommended = false;
diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp
index b4c55b3d..e4a686c2 100644
--- a/launcher/java/JavaChecker.cpp
+++ b/launcher/java/JavaChecker.cpp
@@ -85,7 +85,7 @@ void JavaChecker::performCheck()
process->setProgram(m_path);
process->setProcessChannelMode(QProcess::SeparateChannels);
process->setProcessEnvironment(CleanEnviroment());
- qDebug() << "Running java checker: " + m_path + args.join(" ");;
+ qDebug() << "Running java checker:" << m_path << args.join(" ");
connect(process.get(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &JavaChecker::finished);
connect(process.get(), &QProcess::errorOccurred, this, &JavaChecker::error);
@@ -128,7 +128,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status)
result.outLog = m_stdout;
qDebug() << "STDOUT" << m_stdout;
qWarning() << "STDERR" << m_stderr;
- qDebug() << "Java checker finished with status " << status << " exit code " << exitcode;
+ qDebug() << "Java checker finished with status" << status << "exit code" << exitcode;
if (status == QProcess::CrashExit || exitcode == 1)
{
diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp
index b29af857..3407fdf7 100644
--- a/launcher/java/JavaInstallList.cpp
+++ b/launcher/java/JavaInstallList.cpp
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * 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
@@ -98,6 +99,8 @@ QVariant JavaInstallList::data(const QModelIndex &index, int role) const
auto version = std::dynamic_pointer_cast<JavaInstall>(m_vlist[index.row()]);
switch (role)
{
+ case SortRole:
+ return -index.row();
case VersionPointerRole:
return QVariant::fromValue(m_vlist[index.row()]);
case VersionIdRole:
diff --git a/launcher/meta/JsonFormat.cpp b/launcher/meta/JsonFormat.cpp
index 473f37d6..cb2d06ea 100644
--- a/launcher/meta/JsonFormat.cpp
+++ b/launcher/meta/JsonFormat.cpp
@@ -56,10 +56,10 @@ static Version::Ptr parseCommonVersion(const QString &uid, const QJsonObject &ob
version->setType(ensureString(obj, "type", QString()));
version->setRecommended(ensureBoolean(obj, QString("recommended"), false));
version->setVolatile(ensureBoolean(obj, QString("volatile"), false));
- RequireSet requires, conflicts;
- parseRequires(obj, &requires, "requires");
+ RequireSet reqs, conflicts;
+ parseRequires(obj, &reqs, "requires");
parseRequires(obj, &conflicts, "conflicts");
- version->setRequires(requires, conflicts);
+ version->setRequires(reqs, conflicts);
return version;
}
@@ -176,7 +176,6 @@ void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char * keyName
{
if(obj.contains(keyName))
{
- QSet<QString> requires;
auto reqArray = requireArray(obj, keyName);
auto iter = reqArray.begin();
while(iter != reqArray.end())
diff --git a/launcher/meta/Version.cpp b/launcher/meta/Version.cpp
index e617abf8..0718a420 100644
--- a/launcher/meta/Version.cpp
+++ b/launcher/meta/Version.cpp
@@ -116,9 +116,9 @@ void Meta::Version::setTime(const qint64 time)
emit timeChanged();
}
-void Meta::Version::setRequires(const Meta::RequireSet &requires, const Meta::RequireSet &conflicts)
+void Meta::Version::setRequires(const Meta::RequireSet &reqs, const Meta::RequireSet &conflicts)
{
- m_requires = requires;
+ m_requires = reqs;
m_conflicts = conflicts;
emit requiresChanged();
}
diff --git a/launcher/meta/Version.h b/launcher/meta/Version.h
index 78156193..59a96a68 100644
--- a/launcher/meta/Version.h
+++ b/launcher/meta/Version.h
@@ -63,7 +63,7 @@ public:
{
return m_time;
}
- const Meta::RequireSet &requires() const
+ const Meta::RequireSet &requiredSet() const
{
return m_requires;
}
@@ -91,7 +91,7 @@ public:
public: // for usage by format parsers only
void setType(const QString &type);
void setTime(const qint64 time);
- void setRequires(const Meta::RequireSet &requires, const Meta::RequireSet &conflicts);
+ void setRequires(const Meta::RequireSet &reqs, const Meta::RequireSet &conflicts);
void setVolatile(bool volatile_);
void setRecommended(bool recommended);
void setProvidesRecommendations();
diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp
index 7f001dfc..9f448278 100644
--- a/launcher/meta/VersionList.cpp
+++ b/launcher/meta/VersionList.cpp
@@ -77,7 +77,7 @@ QVariant VersionList::data(const QModelIndex &index, int role) const
case ParentVersionRole:
{
// FIXME: HACK: this should be generic and be replaced by something else. Anything that is a hard 'equals' dep is a 'parent uid'.
- auto & reqs = version->requires();
+ auto & reqs = version->requiredSet();
auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Require & req)
{
return req.uid == "net.minecraft";
@@ -92,7 +92,7 @@ QVariant VersionList::data(const QModelIndex &index, int role) const
case UidRole: return version->uid();
case TimeRole: return version->time();
- case RequiresRole: return QVariant::fromValue(version->requires());
+ case RequiresRole: return QVariant::fromValue(version->requiredSet());
case SortRole: return version->rawTime();
case VersionPtrRole: return QVariant::fromValue(version);
case RecommendedRole: return version->isRecommended();
diff --git a/launcher/minecraft/Component.cpp b/launcher/minecraft/Component.cpp
index 7e5b6058..ff81fcbb 100644
--- a/launcher/minecraft/Component.cpp
+++ b/launcher/minecraft/Component.cpp
@@ -451,9 +451,9 @@ void Component::updateCachedData()
m_cachedVolatile = file->m_volatile;
changed = true;
}
- if(!deepCompare(m_cachedRequires, file->requires))
+ if(!deepCompare(m_cachedRequires, file->m_requires))
{
- m_cachedRequires = file->requires;
+ m_cachedRequires = file->m_requires;
changed = true;
}
if(!deepCompare(m_cachedConflicts, file->conflicts))
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index 2c624a36..f8ed5214 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -149,9 +149,10 @@ void MinecraftInstance::loadSpecificSettings()
// special!
m_settings->registerPassthrough(global_settings->getSetting("JavaTimestamp"), javaOrLocation);
- m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), javaOrLocation);
m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), javaOrLocation);
m_settings->registerPassthrough(global_settings->getSetting("JavaRealArchitecture"), javaOrLocation);
+ m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), javaOrLocation);
+ m_settings->registerPassthrough(global_settings->getSetting("JavaVendor"), javaOrLocation);
// Window Size
auto windowSetting = m_settings->registerSetting("OverrideWindow", false);
diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp
index 888b6860..b586198b 100644
--- a/launcher/minecraft/OneSixVersionFormat.cpp
+++ b/launcher/minecraft/OneSixVersionFormat.cpp
@@ -276,7 +276,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc
if (root.contains("requires"))
{
- Meta::parseRequires(root, &out->requires);
+ Meta::parseRequires(root, &out->m_requires);
}
QString dependsOnMinecraftVersion = root.value("mcVersion").toString();
if(!dependsOnMinecraftVersion.isEmpty())
@@ -284,9 +284,9 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc
Meta::Require mcReq;
mcReq.uid = "net.minecraft";
mcReq.equalsVersion = dependsOnMinecraftVersion;
- if (out->requires.count(mcReq) == 0)
+ if (out->m_requires.count(mcReq) == 0)
{
- out->requires.insert(mcReq);
+ out->m_requires.insert(mcReq);
}
}
if (root.contains("conflicts"))
@@ -392,9 +392,9 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch
}
root.insert("mods", array);
}
- if(!patch->requires.empty())
+ if(!patch->m_requires.empty())
{
- Meta::serializeRequires(root, &patch->requires, "requires");
+ Meta::serializeRequires(root, &patch->m_requires, "requires");
}
if(!patch->conflicts.empty())
{
diff --git a/launcher/minecraft/VersionFile.h b/launcher/minecraft/VersionFile.h
index 11c5a3af..8e9dd167 100644
--- a/launcher/minecraft/VersionFile.h
+++ b/launcher/minecraft/VersionFile.h
@@ -138,7 +138,7 @@ public: /* data */
* Prism Launcher: set of packages this depends on
* NOTE: this is shared with the meta format!!!
*/
- Meta::RequireSet requires;
+ Meta::RequireSet m_requires;
/**
* Prism Launcher: set of packages this conflicts with
diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp
index 9e2fd111..c2794147 100644
--- a/launcher/minecraft/auth/AccountList.cpp
+++ b/launcher/minecraft/auth/AccountList.cpp
@@ -333,13 +333,13 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
case MigrationColumn: {
if(account->isMSA() || account->isOffline()) {
- return tr("N/A", "Can Migrate?");
+ return tr("N/A", "Can Migrate");
}
if (account->canMigrate()) {
- return tr("Yes", "Can Migrate?");
+ return tr("Yes", "Can Migrate");
}
else {
- return tr("No", "Can Migrate?");
+ return tr("No", "Can Migrate");
}
}
diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp
index 5c58f6b2..ca75cd2a 100644
--- a/launcher/minecraft/mod/DataPack.cpp
+++ b/launcher/minecraft/mod/DataPack.cpp
@@ -33,7 +33,9 @@ static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } },
{ 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } },
{ 8, { Version("1.18"), Version("1.18.1") } }, { 9, { Version("1.18.2"), Version("1.18.2") } },
- { 10, { Version("1.19"), Version("1.19.3") } },
+ { 10, { Version("1.19"), Version("1.19.3") } }, { 11, { Version("23w03a"), Version("23w05a") } },
+ { 12, { Version("1.19.4"), Version("1.19.4") } }, { 13, { Version("23w12a"), Version("23w14a") } },
+ { 14, { Version("23w16a"), Version("23w17a") } }, { 15, { Version("1.20"), Version("1.20") } },
};
void DataPack::setPackFormat(int new_format_id)
diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp
index 9aea22ef..e06c1ac1 100644
--- a/launcher/minecraft/mod/ResourcePack.cpp
+++ b/launcher/minecraft/mod/ResourcePack.cpp
@@ -18,7 +18,8 @@ static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } },
{ 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } },
{ 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("22w42a"), Version("22w44a") } },
- { 12, { Version("1.19.3"), Version("1.19.3") } },
+ { 12, { Version("1.19.3"), Version("1.19.3") } }, { 13, { Version("1.19.4"), Version("1.19.4") } },
+ { 14, { Version("1.20"), Version("1.20") } }
};
void ResourcePack::setPackFormat(int new_format_id)
diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h
index 8d0223f9..82da2ab2 100644
--- a/launcher/modplatform/ModIndex.h
+++ b/launcher/modplatform/ModIndex.h
@@ -1,20 +1,20 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
-* PolyMC - Minecraft Launcher
-* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
-*
-* 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/>.
-*/
+ * PolyMC - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ *
+ * 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
@@ -69,7 +69,6 @@ struct IndexedVersion {
// For internal use, not provided by APIs
bool is_currently_selected = false;
- QString custom_target_folder;
};
struct ExtraPackData {
@@ -116,12 +115,12 @@ struct IndexedPack {
if (!versionsLoaded)
return false;
- return std::any_of(versions.constBegin(), versions.constEnd(),
- [](auto const& v) { return v.is_currently_selected; });
+ return std::any_of(versions.constBegin(), versions.constEnd(), [](auto const& v) { return v.is_currently_selected; });
}
};
} // namespace ModPlatform
Q_DECLARE_METATYPE(ModPlatform::IndexedPack)
+Q_DECLARE_METATYPE(ModPlatform::IndexedPack::Ptr)
Q_DECLARE_METATYPE(ModPlatform::ResourceProvider)
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
index 96cea7b7..07e0bf23 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -352,7 +352,7 @@ QString PackInstallTask::getVersionForLoader(QString uid)
if(m_version.loader.recommended || m_version.loader.latest) {
for (int i = 0; i < vlist->versions().size(); i++) {
auto version = vlist->versions().at(i);
- auto reqs = version->requires();
+ auto reqs = version->requiredSet();
// filter by minecraft version, if the loader depends on a certain version.
// not all mod loaders depend on a given Minecraft version, so we won't do this
diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp
index 06a89502..e09aeb3d 100644
--- a/launcher/modplatform/flame/FlameCheckUpdate.cpp
+++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp
@@ -3,6 +3,7 @@
#include "FlameModIndex.h"
#include <MurmurHash2.h>
+#include <memory>
#include "FileSystem.h"
#include "Json.h"
@@ -129,8 +130,7 @@ void FlameCheckUpdate::executeTask()
setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name()));
setProgress(i++, m_mods.size());
- ModPlatform::IndexedPack pack{ mod->metadata()->project_id.toString() };
- auto latest_ver = api.getLatestVersion({ pack, m_game_versions, m_loaders });
+ auto latest_ver = api.getLatestVersion({ { mod->metadata()->project_id.toString() }, m_game_versions, m_loaders });
// Check if we were aborted while getting the latest version
if (m_was_aborted) {
@@ -156,15 +156,15 @@ void FlameCheckUpdate::executeTask()
if (!latest_ver.hash.isEmpty() && (mod->metadata()->hash != latest_ver.hash || mod->status() == ModStatus::NotInstalled)) {
// Fake pack with the necessary info to pass to the download task :)
- ModPlatform::IndexedPack pack;
- pack.name = mod->name();
- pack.slug = mod->metadata()->slug;
- pack.addonId = mod->metadata()->project_id;
- pack.websiteUrl = mod->homeurl();
+ auto pack = std::make_shared<ModPlatform::IndexedPack>();
+ pack->name = mod->name();
+ pack->slug = mod->metadata()->slug;
+ pack->addonId = mod->metadata()->project_id;
+ pack->websiteUrl = mod->homeurl();
for (auto& author : mod->authors())
- pack.authors.append({ author });
- pack.description = mod->description();
- pack.provider = ModPlatform::ResourceProvider::FLAME;
+ pack->authors.append({ author });
+ pack->description = mod->description();
+ pack->provider = ModPlatform::ResourceProvider::FLAME;
auto old_version = mod->version();
if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) {
@@ -173,7 +173,7 @@ void FlameCheckUpdate::executeTask()
}
auto download_task = makeShared<ResourceDownloadTask>(pack, latest_ver, m_mods_folder);
- m_updatable.emplace_back(pack.name, mod->metadata()->hash, old_version, latest_ver.version,
+ m_updatable.emplace_back(pack->name, mod->metadata()->hash, old_version, latest_ver.version,
api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()),
ModPlatform::ResourceProvider::FLAME, download_task);
}
diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
index dae93d1c..35443e71 100644
--- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
+++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
@@ -153,6 +153,9 @@ bool FlameCreationTask::updateInstance()
old_files.remove(file.key());
files_iterator = files.erase(files_iterator);
+
+ if (files_iterator != files.begin())
+ files_iterator--;
}
}
diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
index d1be7209..4fe91ce7 100644
--- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
@@ -54,7 +54,7 @@ void ModrinthCheckUpdate::executeTask()
if (mod->metadata()->hash_format != best_hash_type) {
auto hash_task = Hashing::createModrinthHasher(mod->fileinfo().absoluteFilePath());
connect(hash_task.get(), &Task::succeeded, [&] {
- QString hash (hash_task->getResult());
+ QString hash(hash_task->getResult());
hashes.append(hash);
mappings.insert(hash, mod);
});
@@ -67,7 +67,7 @@ void ModrinthCheckUpdate::executeTask()
}
QEventLoop loop;
- connect(&hashing_task, &Task::finished, [&loop]{ loop.quit(); });
+ connect(&hashing_task, &Task::finished, [&loop] { loop.quit(); });
hashing_task.start();
loop.exec();
@@ -112,7 +112,8 @@ void ModrinthCheckUpdate::executeTask()
// so we may want to filter it
QString loader_filter;
if (m_loaders.has_value()) {
- static auto flags = { ResourceAPI::ModLoaderType::Forge, ResourceAPI::ModLoaderType::Fabric, ResourceAPI::ModLoaderType::Quilt };
+ static auto flags = { ResourceAPI::ModLoaderType::Forge, ResourceAPI::ModLoaderType::Fabric,
+ ResourceAPI::ModLoaderType::Quilt };
for (auto flag : flags) {
if (m_loaders.value().testFlag(flag)) {
loader_filter = api.getModLoaderString(flag);
@@ -122,7 +123,8 @@ void ModrinthCheckUpdate::executeTask()
}
// Currently, we rely on a couple heuristics to determine whether an update is actually available or not:
- // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the loader_filter
+ // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the
+ // loader_filter
// - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case)
// Such is the pain of having arbitrary files for a given version .-.
@@ -149,19 +151,19 @@ void ModrinthCheckUpdate::executeTask()
continue;
// Fake pack with the necessary info to pass to the download task :)
- ModPlatform::IndexedPack pack;
- pack.name = mod->name();
- pack.slug = mod->metadata()->slug;
- pack.addonId = mod->metadata()->project_id;
- pack.websiteUrl = mod->homeurl();
+ auto pack = std::make_shared<ModPlatform::IndexedPack>();
+ pack->name = mod->name();
+ pack->slug = mod->metadata()->slug;
+ pack->addonId = mod->metadata()->project_id;
+ pack->websiteUrl = mod->homeurl();
for (auto& author : mod->authors())
- pack.authors.append({ author });
- pack.description = mod->description();
- pack.provider = ModPlatform::ResourceProvider::MODRINTH;
+ pack->authors.append({ author });
+ pack->description = mod->description();
+ pack->provider = ModPlatform::ResourceProvider::MODRINTH;
auto download_task = makeShared<ResourceDownloadTask>(pack, project_ver, m_mods_folder);
- m_updatable.emplace_back(pack.name, hash, mod->version(), project_ver.version_number, project_ver.changelog,
+ m_updatable.emplace_back(pack->name, hash, mod->version(), project_ver.version_number, project_ver.changelog,
ModPlatform::ResourceProvider::MODRINTH, download_task);
}
}
diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
index bb8227aa..76f07277 100644
--- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
+++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
@@ -214,7 +214,7 @@ bool ModrinthCreationTask::createInstance()
if (m_instIcon != "default") {
instance.setIconKey(m_instIcon);
- } else {
+ } else if (!m_managed_id.isEmpty()) {
instance.setIconKey("modrinth");
}
diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
new file mode 100644
index 00000000..bff9bf42
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
@@ -0,0 +1,319 @@
+// 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); }))
+ 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;
+ file["path"] = iterator.key();
+ 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);
+}
diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h
new file mode 100644
index 00000000..af00ffaa
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h
@@ -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();
+};
diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h
index 501318a1..728193b3 100644
--- a/launcher/net/ByteArraySink.h
+++ b/launcher/net/ByteArraySink.h
@@ -53,7 +53,10 @@ class ByteArraySink : public Sink {
public:
auto init(QNetworkRequest& request) -> Task::State override
{
- m_output->clear();
+ if (m_output)
+ m_output->clear();
+ else
+ qWarning() << "ByteArraySink did not initialize the buffer because it's not addressable";
if (initAllValidators(request))
return Task::State::Running;
return Task::State::Failed;
@@ -61,7 +64,10 @@ class ByteArraySink : public Sink {
auto write(QByteArray& data) -> Task::State override
{
- m_output->append(data);
+ if (m_output)
+ m_output->append(data);
+ else
+ qWarning() << "ByteArraySink did not write the buffer because it's not addressable";
if (writeAllValidators(data))
return Task::State::Running;
return Task::State::Failed;
@@ -69,7 +75,10 @@ class ByteArraySink : public Sink {
auto abort() -> Task::State override
{
- m_output->clear();
+ if (m_output)
+ m_output->clear();
+ else
+ qWarning() << "ByteArraySink did not clear the buffer because it's not addressable";
failAllValidators();
return Task::State::Failed;
}
diff --git a/launcher/settings/INIFile.cpp b/launcher/settings/INIFile.cpp
index f0347cab..d16256b9 100644
--- a/launcher/settings/INIFile.cpp
+++ b/launcher/settings/INIFile.cpp
@@ -45,12 +45,12 @@
#include <QSettings>
-INIFile::INIFile()
-{
-}
+INIFile::INIFile() {}
bool INIFile::saveFile(QString fileName)
{
+ if (!contains("ConfigVersion"))
+ insert("ConfigVersion", "1.2");
QSettings _settings_obj{ fileName, QSettings::Format::IniFormat };
_settings_obj.setFallbacksEnabled(false);
@@ -71,6 +71,81 @@ bool INIFile::saveFile(QString fileName)
return true;
}
+QString unescape(QString orig)
+{
+ QString out;
+ QChar prev = QChar::Null;
+ for (auto c : orig) {
+ if (prev == '\\') {
+ if (c == 'n')
+ out += '\n';
+ else if (c == 't')
+ out += '\t';
+ else if (c == '#')
+ out += '#';
+ else
+ out += c;
+ prev = QChar::Null;
+ } else {
+ if (c == '\\') {
+ prev = c;
+ continue;
+ }
+ out += c;
+ prev = QChar::Null;
+ }
+ }
+ return out;
+}
+
+QString unquote(QString str)
+{
+ if ((str.contains(QChar(';')) || str.contains(QChar('=')) || str.contains(QChar(','))) && str.endsWith("\"") && str.startsWith("\"")) {
+#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
+ str = str.remove(0, 1);
+ str = str.remove(str.size() - 1, 1);
+#else
+ str = str.removeFirst().removeLast();
+#endif
+ }
+ return str;
+}
+
+bool parseOldFileFormat(QIODevice& device, QSettings::SettingsMap& map)
+{
+ QTextStream in(device.readAll());
+#if QT_VERSION <= QT_VERSION_CHECK(6, 0, 0)
+ in.setCodec("UTF-8");
+#endif
+
+ QStringList lines = in.readAll().split('\n');
+ for (int i = 0; i < lines.count(); i++) {
+ QString& lineRaw = lines[i];
+ // Ignore comments.
+ int commentIndex = 0;
+ QString line = lineRaw;
+ // Search for comments until no more escaped # are available
+ while ((commentIndex = line.indexOf('#', commentIndex + 1)) != -1) {
+ if (commentIndex > 0 && line.at(commentIndex - 1) == '\\') {
+ continue;
+ }
+ line = line.left(lineRaw.indexOf('#')).trimmed();
+ }
+
+ int eqPos = line.indexOf('=');
+ if (eqPos == -1)
+ continue;
+ QString key = line.left(eqPos).trimmed();
+ QString valueStr = line.right(line.length() - eqPos - 1).trimmed();
+
+ valueStr = unquote(unescape(valueStr));
+
+ QVariant value(valueStr);
+ map.insert(key, value);
+ }
+
+ return true;
+}
bool INIFile::loadFile(QString fileName)
{
@@ -84,10 +159,29 @@ bool INIFile::loadFile(QString fileName)
qCritical() << "A format error occurred (e.g. loading a malformed INI file).";
return false;
}
-
- for (auto&& key : _settings_obj.allKeys())
- insert(key, _settings_obj.value(key));
-
+ if (!_settings_obj.value("ConfigVersion").isValid()) {
+ QFile file(fileName);
+ if (!file.open(QIODevice::ReadOnly))
+ return false;
+ QSettings::SettingsMap map;
+ parseOldFileFormat(file, map);
+ file.close();
+ for (auto&& key : map.keys())
+ insert(key, map.value(key));
+ insert("ConfigVersion", "1.2");
+ } else if (_settings_obj.value("ConfigVersion").toString() == "1.1") {
+ for (auto&& key : _settings_obj.allKeys()) {
+ if (auto valueStr = _settings_obj.value(key).toString();
+ (valueStr.contains(QChar(';')) || valueStr.contains(QChar('=')) || valueStr.contains(QChar(','))) &&
+ valueStr.endsWith("\"") && valueStr.startsWith("\"")) {
+ insert(key, unquote(valueStr));
+ } else
+ insert(key, _settings_obj.value(key));
+ }
+ insert("ConfigVersion", "1.2");
+ } else
+ for (auto&& key : _settings_obj.allKeys())
+ insert(key, _settings_obj.value(key));
return true;
}
@@ -103,4 +197,3 @@ void INIFile::set(QString key, QVariant val)
{
this->operator[](key) = val;
}
-
diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp
index 5ee14505..9aada5e6 100644
--- a/launcher/tasks/ConcurrentTask.cpp
+++ b/launcher/tasks/ConcurrentTask.cpp
@@ -138,19 +138,18 @@ void ConcurrentTask::startNext()
connect(next.get(), &Task::progress, this, [this, next](qint64 current, qint64 total) { subTaskProgress(next, current, total); });
m_doing.insert(next.get(), next);
+ qsizetype num_starts = qMin(m_queue.size(), m_total_max_size - m_doing.size());
auto task_progress = std::make_shared<TaskStepProgress>(next->getUid());
m_task_progress.insert(next->getUid(), task_progress);
updateState();
updateStepProgress(*task_progress.get(), Operation::ADDED);
-
QCoreApplication::processEvents();
QMetaObject::invokeMethod(next.get(), &Task::start, Qt::QueuedConnection);
// Allow going up the number of concurrent tasks in case of tasks being added in the middle of a running task.
- int num_starts = qMin(m_queue.size(), m_total_max_size - m_doing.size());
for (int i = 0; i < num_starts; i++)
QMetaObject::invokeMethod(this, &ConcurrentTask::startNext, Qt::QueuedConnection);
}
diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp
index 46db4804..23e55c51 100644
--- a/launcher/translations/TranslationsModel.cpp
+++ b/launcher/translations/TranslationsModel.cpp
@@ -190,7 +190,7 @@ struct TranslationsModel::Private
std::unique_ptr<QTranslator> m_qt_translator;
std::unique_ptr<QTranslator> m_app_translator;
- Net::Download::Ptr m_index_task;
+ Net::Download* m_index_task;
QString m_downloadingTranslation;
NetJob::Ptr m_dl_job;
NetJob::Ptr m_index_job;
@@ -673,8 +673,9 @@ void TranslationsModel::downloadIndex()
d->m_index_job.reset(new NetJob("Translations Index", APPLICATION->network()));
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json");
entry->setStale(true);
- d->m_index_task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + "index_v2.json"), entry);
- d->m_index_job->addNetAction(d->m_index_task);
+ auto task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + "index_v2.json"), entry);
+ d->m_index_task = task.get();
+ d->m_index_job->addNetAction(task);
connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed);
connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived);
d->m_index_job->start();
diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp
index 72b7db64..e04011ca 100644
--- a/launcher/ui/MainWindow.cpp
+++ b/launcher/ui/MainWindow.cpp
@@ -2,7 +2,7 @@
/*
* Prism Launcher - Minecraft Launcher
* 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
* 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/EditAccountDialog.h"
#include "ui/dialogs/ExportInstanceDialog.h"
+#include "ui/dialogs/ExportMrPackDialog.h"
#include "ui/dialogs/ImportResourceDialog.h"
#include "ui/themes/ITheme.h"
#include "ui/themes/ThemeManager.h"
@@ -186,7 +187,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
}
- // set the menu for the folders help, and accounts tool buttons
+ // set the menu for the folders help, accounts, and export tool buttons
{
auto foldersMenuButton = dynamic_cast<QToolButton*>(ui->mainToolBar->widgetForAction(ui->actionFoldersButton));
ui->actionFoldersButton->setMenu(ui->foldersMenu);
@@ -199,8 +200,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
helpMenuButton->setPopupMode(QToolButton::InstantPopup);
auto accountMenuButton = dynamic_cast<QToolButton*>(ui->mainToolBar->widgetForAction(ui->actionAccountsButton));
- ui->actionAccountsButton->setMenu(ui->accountsMenu);
accountMenuButton->setPopupMode(QToolButton::InstantPopup);
+
+ auto exportInstanceMenu = new QMenu(this);
+ exportInstanceMenu->addAction(ui->actionExportInstanceZip);
+ exportInstanceMenu->addAction(ui->actionExportInstanceMrPack);
+ ui->actionExportInstance->setMenu(exportInstanceMenu);
}
// hide, disable and show stuff
@@ -414,15 +419,6 @@ void MainWindow::keyReleaseEvent(QKeyEvent *event)
void MainWindow::retranslateUi()
{
- auto accounts = APPLICATION->accounts();
- MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
- if(defaultAccount) {
- auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse());
- ui->actionAccountsButton->setText(profileLabel);
- }
- else {
- ui->actionAccountsButton->setText(tr("Accounts"));
- }
if (m_selectedInstance) {
m_statusLeft->setText(m_selectedInstance->getStatusbarDescription());
@@ -432,6 +428,12 @@ void MainWindow::retranslateUi()
ui->retranslateUi(this);
+ MinecraftAccountPtr defaultAccount = APPLICATION->accounts()->defaultAccount();
+ if(defaultAccount) {
+ auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse());
+ ui->actionAccountsButton->setText(profileLabel);
+ }
+
changeIconButton->setToolTip(ui->actionChangeInstIcon->toolTip());
renameButton->setToolTip(ui->actionRenameInstance->toolTip());
@@ -471,7 +473,23 @@ void MainWindow::lockToolbars(bool state)
void MainWindow::konamiTriggered()
{
- qDebug() << "Super Secret Mode ACTIVATED!";
+ QString gradient = " stop:0 rgba(125, 0, 0, 255), stop:0.166 rgba(125, 125, 0, 255), stop:0.333 rgba(0, 125, 0, 255), stop:0.5 rgba(0, 125, 125, 255), stop:0.666 rgba(0, 0, 125, 255), stop:0.833 rgba(125, 0, 125, 255), stop:1 rgba(125, 0, 0, 255));";
+ QString stylesheet = "background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0," + gradient;
+ if (ui->mainToolBar->styleSheet() == stylesheet) {
+ ui->mainToolBar->setStyleSheet("");
+ ui->instanceToolBar->setStyleSheet("");
+ ui->centralWidget->setStyleSheet("");
+ ui->newsToolBar->setStyleSheet("");
+ ui->statusBar->setStyleSheet("");
+ qDebug() << "Super Secret Mode DEACTIVATED!";
+ } else {
+ ui->mainToolBar->setStyleSheet(stylesheet);
+ ui->instanceToolBar->setStyleSheet("background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1," + gradient);
+ ui->centralWidget->setStyleSheet("background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1," + gradient);
+ ui->newsToolBar->setStyleSheet(stylesheet);
+ ui->statusBar->setStyleSheet(stylesheet);
+ qDebug() << "Super Secret Mode ACTIVATED!";
+ }
}
void MainWindow::showInstanceContextMenu(const QPoint &pos)
@@ -673,6 +691,15 @@ void MainWindow::repopulateAccountsMenu()
{
ui->accountsMenu->clear();
+ // NOTE: this is done so the accounts button text is not set to the accounts menu title
+ QMenu *accountsButtonMenu = ui->actionAccountsButton->menu();
+ if (accountsButtonMenu) {
+ accountsButtonMenu->clear();
+ } else {
+ accountsButtonMenu = new QMenu(this);
+ ui->actionAccountsButton->setMenu(accountsButtonMenu);
+ }
+
auto accounts = APPLICATION->accounts();
MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
@@ -687,6 +714,8 @@ void MainWindow::repopulateAccountsMenu()
}
}
+ QActionGroup* accountsGroup = new QActionGroup(this);
+
if (accounts->count() <= 0)
{
ui->actionNoAccountsAdded->setEnabled(false);
@@ -702,6 +731,7 @@ void MainWindow::repopulateAccountsMenu()
QAction *action = new QAction(profileLabel, this);
action->setData(i);
action->setCheckable(true);
+ action->setActionGroup(accountsGroup);
if (defaultAccount == account)
{
action->setChecked(true);
@@ -730,6 +760,7 @@ void MainWindow::repopulateAccountsMenu()
ui->actionNoDefaultAccount->setData(-1);
ui->actionNoDefaultAccount->setChecked(!defaultAccount);
+ ui->actionNoDefaultAccount->setActionGroup(accountsGroup);
ui->accountsMenu->addAction(ui->actionNoDefaultAccount);
@@ -737,6 +768,8 @@ void MainWindow::repopulateAccountsMenu()
ui->accountsMenu->addSeparator();
ui->accountsMenu->addAction(ui->actionManageAccounts);
+
+ accountsButtonMenu->addActions(ui->accountsMenu->actions());
}
void MainWindow::updatesAllowedChanged(bool allowed)
@@ -1201,6 +1234,12 @@ void MainWindow::on_actionViewInstanceFolder_triggered()
DesktopServices::openDirectory(str);
}
+void MainWindow::on_actionViewLauncherRootFolder_triggered()
+{
+ const QString dataPath = QDir::currentPath();
+ DesktopServices::openDirectory(dataPath);
+}
+
void MainWindow::refreshInstances()
{
APPLICATION->instances()->loadList();
@@ -1359,7 +1398,7 @@ void MainWindow::on_actionDeleteInstance_triggered()
APPLICATION->instances()->deleteInstance(id);
}
-void MainWindow::on_actionExportInstance_triggered()
+void MainWindow::on_actionExportInstanceZip_triggered()
{
if (m_selectedInstance)
{
@@ -1368,6 +1407,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()
{
if (m_selectedInstance)
diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h
index 3a42c34e..3bb20c4a 100644
--- a/launcher/ui/MainWindow.h
+++ b/launcher/ui/MainWindow.h
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * 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
@@ -112,6 +113,8 @@ private slots:
void on_actionViewInstanceFolder_triggered();
+ void on_actionViewLauncherRootFolder_triggered();
+
void on_actionViewSelectedInstFolder_triggered();
void refreshInstances();
@@ -151,7 +154,9 @@ private slots:
void deleteGroup();
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();
diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui
index 2b6a10b1..f67fb185 100644
--- a/launcher/ui/MainWindow.ui
+++ b/launcher/ui/MainWindow.ui
@@ -187,6 +187,7 @@
<bool>true</bool>
</property>
<addaction name="actionViewInstanceFolder"/>
+ <addaction name="actionViewLauncherRootFolder"/>
<addaction name="actionViewCentralModsFolder"/>
</widget>
<widget class="QMenu" name="accountsMenu">
@@ -459,10 +460,23 @@
<string>E&amp;xport...</string>
</property>
<property name="toolTip">
- <string>Export the selected instance as a zip file.</string>
+ <string>Export the selected instance to supported formats.</string>
</property>
- <property name="shortcut">
- <string>Ctrl+E</string>
+ </action>
+ <action name="actionExportInstanceZip">
+ <property name="icon">
+ <iconset theme="launcher"/>
+ </property>
+ <property name="text">
+ <string>Prism Launcher (zip)</string>
+ </property>
+ </action>
+ <action name="actionExportInstanceMrPack">
+ <property name="icon">
+ <iconset theme="modrinth"/>
+ </property>
+ <property name="text">
+ <string>Modrinth (mrpack)</string>
</property>
</action>
<action name="actionCreateInstanceShortcut">
@@ -528,6 +542,18 @@
<string>Open the instance folder in a file browser.</string>
</property>
</action>
+ <action name="actionViewLauncherRootFolder">
+ <property name="icon">
+ <iconset theme="viewfolder">
+ <normaloff>.</normaloff>.</iconset>
+ </property>
+ <property name="text">
+ <string>&amp;View Launcher Root Folder</string>
+ </property>
+ <property name="toolTip">
+ <string>Open the launcher's root folder in a file browser.</string>
+ </property>
+ </action>
<action name="actionViewCentralModsFolder">
<property name="icon">
<iconset theme="centralmods">
diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp
index 07ec3c70..8ecd91a9 100644
--- a/launcher/ui/dialogs/ExportInstanceDialog.cpp
+++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * 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
@@ -46,301 +47,21 @@
#include <QSaveFile>
#include <QStack>
#include <QFileInfo>
-
-#include "StringUtils.h"
#include "SeparatorPrefixTree.h"
#include "Application.h"
#include <icons/IconList.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)
: QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance)
{
ui->setupUi(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();
proxyModel->setSourceModel(model);
- auto root = instance->instanceRoot();
ui->treeView->setModel(proxyModel);
ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root)));
ui->treeView->sortByColumn(0, Qt::AscendingOrder);
@@ -404,22 +125,11 @@ bool ExportInstanceDialog::doExport()
const QString output = QFileDialog::getSaveFileName(
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())
{
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);
@@ -511,5 +221,3 @@ void ExportInstanceDialog::savePackIgnore()
qWarning() << e.cause();
}
}
-
-#include "ExportInstanceDialog.moc"
diff --git a/launcher/ui/dialogs/ExportInstanceDialog.h b/launcher/ui/dialogs/ExportInstanceDialog.h
index dea02d1b..5e801875 100644
--- a/launcher/ui/dialogs/ExportInstanceDialog.h
+++ b/launcher/ui/dialogs/ExportInstanceDialog.h
@@ -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");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * 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.
*
- * 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
- * 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.
+ * 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
@@ -18,9 +38,10 @@
#include <QDialog>
#include <QModelIndex>
#include <memory>
+#include "FileIgnoreProxy.h"
+#include "FastFileIconProvider.h"
class BaseInstance;
-class PackIgnoreProxy;
typedef std::shared_ptr<BaseInstance> InstancePtr;
namespace Ui
@@ -47,7 +68,8 @@ private:
private:
Ui::ExportInstanceDialog *ui;
InstancePtr m_instance;
- PackIgnoreProxy * proxyModel;
+ FileIgnoreProxy * proxyModel;
+ FastFileIconProvider icons;
private slots:
void rowsInserted(QModelIndex parent, int top, int bottom);
diff --git a/launcher/ui/dialogs/ExportMrPackDialog.cpp b/launcher/ui/dialogs/ExportMrPackDialog.cpp
new file mode 100644
index 00000000..561b92e4
--- /dev/null
+++ b/launcher/ui/dialogs/ExportMrPackDialog.cpp
@@ -0,0 +1,124 @@
+// 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 <QPushButton>
+#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]);
+
+ // ensure a valid pack is generated
+ // the name and version fields mustn't be empty
+ connect(ui->name, &QLineEdit::textEdited, this, &ExportMrPackDialog::validate);
+ connect(ui->version, &QLineEdit::textEdited, this, &ExportMrPackDialog::validate);
+ // the instance name can technically be empty
+ validate();
+
+ 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);
+ proxy->setFilterRegularExpression("^(?!\\.DS_Store).+$");
+
+ 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);
+}
+
+void ExportMrPackDialog::validate()
+{
+ const bool invalid = ui->name->text().isEmpty() || ui->version->text().isEmpty();
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(invalid);
+}
diff --git a/launcher/ui/dialogs/ExportMrPackDialog.h b/launcher/ui/dialogs/ExportMrPackDialog.h
new file mode 100644
index 00000000..1c70c4ae
--- /dev/null
+++ b/launcher/ui/dialogs/ExportMrPackDialog.h
@@ -0,0 +1,45 @@
+// 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;
+ void validate();
+
+ private:
+ const InstancePtr instance;
+ Ui::ExportMrPackDialog* ui;
+ FileIgnoreProxy* proxy;
+ FastFileIconProvider icons;
+};
diff --git a/launcher/ui/dialogs/ExportMrPackDialog.ui b/launcher/ui/dialogs/ExportMrPackDialog.ui
new file mode 100644
index 00000000..9a789737
--- /dev/null
+++ b/launcher/ui/dialogs/ExportMrPackDialog.ui
@@ -0,0 +1,136 @@
+<?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">
+ <property name="text">
+ <string>1.0.0</string>
+ </property>
+ </widget>
+ </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>
diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp
index d2a8d33e..6d90480f 100644
--- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp
+++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp
@@ -20,14 +20,15 @@
#include "ResourceDownloadDialog.h"
#include <QPushButton>
+#include <algorithm>
#include "Application.h"
#include "ResourceDownloadTask.h"
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/ResourcePackFolderModel.h"
-#include "minecraft/mod/TexturePackFolderModel.h"
#include "minecraft/mod/ShaderPackFolderModel.h"
+#include "minecraft/mod/TexturePackFolderModel.h"
#include "ui/dialogs/ReviewMessageBox.h"
@@ -41,7 +42,10 @@
namespace ResourceDownload {
ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::shared_ptr<ResourceFolderModel> base_model)
- : QDialog(parent), m_base_model(base_model), m_buttons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel), m_vertical_layout(this)
+ : QDialog(parent)
+ , m_base_model(base_model)
+ , m_buttons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel)
+ , m_vertical_layout(this)
{
setObjectName(QStringLiteral("ResourceDownloadDialog"));
@@ -102,7 +106,8 @@ void ResourceDownloadDialog::initializeContainer()
void ResourceDownloadDialog::connectButtons()
{
auto OkButton = m_buttons.button(QDialogButtonBox::Ok);
- OkButton->setToolTip(tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return").arg(resourcesString()));
+ OkButton->setToolTip(
+ tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return").arg(resourcesString()));
connect(OkButton, &QPushButton::clicked, this, &ResourceDownloadDialog::confirm);
auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel);
@@ -114,21 +119,24 @@ void ResourceDownloadDialog::connectButtons()
void ResourceDownloadDialog::confirm()
{
- auto keys = m_selected.keys();
- keys.sort(Qt::CaseInsensitive);
+ auto selected = getTasks();
+ std::sort(selected.begin(), selected.end(), [](const DownloadTaskPtr& a, const DownloadTaskPtr& b) {
+ return QString::compare(a->getName(), b->getName(), Qt::CaseInsensitive) < 0;
+ });
auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString()));
confirm_dialog->retranslateUi(resourcesString());
- for (auto& task : keys) {
- auto selected = m_selected.constFind(task).value();
- confirm_dialog->appendResource({ task, selected->getFilename(), selected->getCustomPath() });
+ for (auto& task : selected) {
+ confirm_dialog->appendResource({ task->getName(), task->getFilename(), task->getCustomPath() });
}
if (confirm_dialog->exec()) {
auto deselected = confirm_dialog->deselectedResources();
- for (auto name : deselected) {
- m_selected.remove(name);
+ for (auto page : m_container->getPages()) {
+ auto res = static_cast<ResourcePage*>(page);
+ for (auto name : deselected)
+ res->removeResourceFromPage(name);
}
this->accept();
@@ -145,46 +153,39 @@ ResourcePage* ResourceDownloadDialog::getSelectedPage()
return m_selectedPage;
}
-void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, bool is_indexed)
+void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& ver)
{
- removeResource(pack, ver);
-
- ver.is_currently_selected = true;
- m_selected.insert(pack.name, makeShared<ResourceDownloadTask>(pack, ver, getBaseModel(), is_indexed));
-
- m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty());
+ removeResource(pack->name);
+ m_selectedPage->addResourceToPage(pack, ver, getBaseModel());
+ setButtonStatus();
}
-static ModPlatform::IndexedVersion& getVersionWithID(ModPlatform::IndexedPack& pack, QVariant id)
+void ResourceDownloadDialog::removeResource(const QString& pack_name)
{
- Q_ASSERT(pack.versionsLoaded);
- auto it = std::find_if(pack.versions.begin(), pack.versions.end(), [id](auto const& v) { return v.fileId == id; });
- Q_ASSERT(it != pack.versions.end());
- return *it;
+ for (auto page : m_container->getPages()) {
+ static_cast<ResourcePage*>(page)->removeResourceFromPage(pack_name);
+ }
+ setButtonStatus();
}
-void ResourceDownloadDialog::removeResource(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver)
+void ResourceDownloadDialog::setButtonStatus()
{
- if (auto selected_task_it = m_selected.find(pack.name); selected_task_it != m_selected.end()) {
- auto selected_task = *selected_task_it;
- auto old_version_id = selected_task->getVersionID();
-
- // If the new and old version IDs don't match, search for the old one and deselect it.
- if (ver.fileId != old_version_id)
- getVersionWithID(pack, old_version_id).is_currently_selected = false;
+ auto selected = false;
+ for (auto page : m_container->getPages()) {
+ auto res = static_cast<ResourcePage*>(page);
+ selected = selected || res->hasSelectedPacks();
}
-
- // Deselect the new version too, since all versions of that pack got removed.
- ver.is_currently_selected = false;
-
- m_selected.remove(pack.name);
-
- m_buttons.button(QDialogButtonBox::Ok)->setEnabled(!m_selected.isEmpty());
+ m_buttons.button(QDialogButtonBox::Ok)->setEnabled(selected);
}
const QList<ResourceDownloadDialog::DownloadTaskPtr> ResourceDownloadDialog::getTasks()
{
- return m_selected.values();
+ QList<DownloadTaskPtr> selected;
+ for (auto page : m_container->getPages()) {
+ auto res = static_cast<ResourcePage*>(page);
+ selected.append(res->selectedPacks());
+ }
+ return selected;
}
void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected)
@@ -205,8 +206,6 @@ void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* s
m_selectedPage->setSearchTerm(prev_page->getSearchTerm());
}
-
-
ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr<ModFolderModel>& mods, BaseInstance* instance)
: ResourceDownloadDialog(parent, mods), m_instance(instance)
{
@@ -232,7 +231,6 @@ QList<BasePage*> ModDownloadDialog::getPages()
return pages;
}
-
ResourcePackDownloadDialog::ResourcePackDownloadDialog(QWidget* parent,
const std::shared_ptr<ResourcePackFolderModel>& resource_packs,
BaseInstance* instance)
@@ -255,10 +253,11 @@ QList<BasePage*> ResourcePackDownloadDialog::getPages()
if (APPLICATION->capabilities() & Application::SupportsFlame)
pages.append(FlameResourcePackPage::create(this, *m_instance));
+ m_selectedPage = dynamic_cast<ResourcePackResourcePage*>(pages[0]);
+
return pages;
}
-
TexturePackDownloadDialog::TexturePackDownloadDialog(QWidget* parent,
const std::shared_ptr<TexturePackFolderModel>& resource_packs,
BaseInstance* instance)
@@ -281,10 +280,11 @@ QList<BasePage*> TexturePackDownloadDialog::getPages()
if (APPLICATION->capabilities() & Application::SupportsFlame)
pages.append(FlameTexturePackPage::create(this, *m_instance));
+ m_selectedPage = dynamic_cast<TexturePackResourcePage*>(pages[0]);
+
return pages;
}
-
ShaderPackDownloadDialog::ShaderPackDownloadDialog(QWidget* parent,
const std::shared_ptr<ShaderPackFolderModel>& shaders,
BaseInstance* instance)
@@ -305,6 +305,8 @@ QList<BasePage*> ShaderPackDownloadDialog::getPages()
pages.append(ModrinthShaderPackPage::create(this, *m_instance));
+ m_selectedPage = dynamic_cast<ShaderPackResourcePage*>(pages[0]);
+
return pages;
}
diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h
index 5678dc8b..5b5b48c6 100644
--- a/launcher/ui/dialogs/ResourceDownloadDialog.h
+++ b/launcher/ui/dialogs/ResourceDownloadDialog.h
@@ -62,8 +62,8 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider {
bool selectPage(QString pageId);
ResourcePage* getSelectedPage();
- void addResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&, bool is_indexed = false);
- void removeResource(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
+ void addResource(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&);
+ void removeResource(const QString&);
const QList<DownloadTaskPtr> getTasks();
[[nodiscard]] const std::shared_ptr<ResourceFolderModel> getBaseModel() const { return m_base_model; }
@@ -79,6 +79,7 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider {
protected:
[[nodiscard]] virtual QString geometrySaveKey() const { return ""; }
+ void setButtonStatus();
protected:
const std::shared_ptr<ResourceFolderModel> m_base_model;
@@ -88,12 +89,8 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider {
QDialogButtonBox m_buttons;
QVBoxLayout m_vertical_layout;
-
- QHash<QString, DownloadTaskPtr> m_selected;
};
-
-
class ModDownloadDialog final : public ResourceDownloadDialog {
Q_OBJECT
@@ -135,8 +132,8 @@ class TexturePackDownloadDialog final : public ResourceDownloadDialog {
public:
explicit TexturePackDownloadDialog(QWidget* parent,
- const std::shared_ptr<TexturePackFolderModel>& resource_packs,
- BaseInstance* instance);
+ const std::shared_ptr<TexturePackFolderModel>& resource_packs,
+ BaseInstance* instance);
~TexturePackDownloadDialog() override = default;
//: String that gets appended to the texture pack download dialog title ("Download " + resourcesString())
@@ -153,9 +150,7 @@ class ShaderPackDownloadDialog final : public ResourceDownloadDialog {
Q_OBJECT
public:
- explicit ShaderPackDownloadDialog(QWidget* parent,
- const std::shared_ptr<ShaderPackFolderModel>& shader_packs,
- BaseInstance* instance);
+ explicit ShaderPackDownloadDialog(QWidget* parent, const std::shared_ptr<ShaderPackFolderModel>& shader_packs, BaseInstance* instance);
~ShaderPackDownloadDialog() override = default;
//: String that gets appended to the shader pack download dialog title ("Download " + resourcesString())
diff --git a/launcher/ui/dialogs/VersionSelectDialog.cpp b/launcher/ui/dialogs/VersionSelectDialog.cpp
index d7880334..5feb70d2 100644
--- a/launcher/ui/dialogs/VersionSelectDialog.cpp
+++ b/launcher/ui/dialogs/VersionSelectDialog.cpp
@@ -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");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * 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.
*
- * 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
- * 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.
+ * 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 "VersionSelectDialog.h"
@@ -22,15 +42,10 @@
#include <QtWidgets/QVBoxLayout>
#include <QDebug>
-#include "ui/dialogs/ProgressDialog.h"
#include "ui/widgets/VersionSelectWidget.h"
-#include "ui/dialogs/CustomMessageBox.h"
#include "BaseVersion.h"
#include "BaseVersionList.h"
-#include "tasks/Task.h"
-#include "Application.h"
-#include "VersionProxyModel.h"
VersionSelectDialog::VersionSelectDialog(BaseVersionList *vlist, QString title, QWidget *parent, bool cancelable)
: QDialog(parent)
@@ -40,7 +55,7 @@ VersionSelectDialog::VersionSelectDialog(BaseVersionList *vlist, QString title,
m_verticalLayout = new QVBoxLayout(this);
m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
- m_versionWidget = new VersionSelectWidget(parent);
+ m_versionWidget = new VersionSelectWidget(true, parent);
m_verticalLayout->addWidget(m_versionWidget);
m_horizontalLayout = new QHBoxLayout();
diff --git a/launcher/ui/pages/BasePage.h b/launcher/ui/pages/BasePage.h
index ceb24040..5537c28f 100644
--- a/launcher/ui/pages/BasePage.h
+++ b/launcher/ui/pages/BasePage.h
@@ -35,15 +35,16 @@
#pragma once
-#include <QString>
#include <QIcon>
+#include <QString>
+#include <functional>
#include <memory>
#include "BasePageContainer.h"
-class BasePage
-{
-public:
+class BasePage {
+ public:
+ using updateExtraInfoFunc = std::function<void(QString)>;
virtual ~BasePage() {}
virtual QString id() const = 0;
virtual QString displayName() const = 0;
@@ -63,17 +64,16 @@ public:
}
virtual void openedImpl() {}
virtual void closedImpl() {}
- virtual void setParentContainer(BasePageContainer * container)
- {
- m_container = container;
- };
- virtual void retranslate() { }
+ virtual void setParentContainer(BasePageContainer* container) { m_container = container; };
+ virtual void retranslate() {}
-public:
+ public:
int stackIndex = -1;
int listIndex = -1;
-protected:
- BasePageContainer * m_container = nullptr;
+ updateExtraInfoFunc updateExtraInfo;
+
+ protected:
+ BasePageContainer* m_container = nullptr;
bool isOpened = false;
};
diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp
index f662ee1c..dca1b3a6 100644
--- a/launcher/ui/pages/global/APIPage.cpp
+++ b/launcher/ui/pages/global/APIPage.cpp
@@ -177,7 +177,7 @@ void APIPage::applySettings()
metaURL.setScheme("https");
}
- s->set("MetaURLOverride", metaURL);
+ s->set("MetaURLOverride", metaURL.toString());
QString flameKey = ui->flameKey->text();
s->set("FlameKeyOverride", flameKey);
QString modrinthToken = ui->modrinthToken->text();
diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui
index 55bd3eea..d9116bfc 100644
--- a/launcher/ui/pages/global/LauncherPage.ui
+++ b/launcher/ui/pages/global/LauncherPage.ui
@@ -172,7 +172,7 @@
<string>Disable using metadata provided by mod providers (like Modrinth or Curseforge) for mods.</string>
</property>
<property name="text">
- <string>Disable using metadata for mods?</string>
+ <string>Disable using metadata for mods</string>
</property>
</widget>
</item>
@@ -307,21 +307,21 @@
<item>
<widget class="QCheckBox" name="showConsoleCheck">
<property name="text">
- <string>Show console while the game is &amp;running?</string>
+ <string>Show console while the game is &amp;running</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="autoCloseConsoleCheck">
<property name="text">
- <string>&amp;Automatically close console when the game quits?</string>
+ <string>&amp;Automatically close console when the game quits</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showConsoleErrorCheck">
<property name="text">
- <string>Show console when the game &amp;crashes?</string>
+ <string>Show console when the game &amp;crashes</string>
</property>
</widget>
</item>
diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui
index 103881b5..8f5de725 100644
--- a/launcher/ui/pages/global/MinecraftPage.ui
+++ b/launcher/ui/pages/global/MinecraftPage.ui
@@ -51,7 +51,7 @@
<item>
<widget class="QCheckBox" name="maximizedCheckBox">
<property name="text">
- <string>Start Minecraft &amp;maximized?</string>
+ <string>Start Minecraft &amp;maximized</string>
</property>
</widget>
</item>
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp
index 2f824ffb..8e5226ef 100644
--- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp
@@ -1,3 +1,38 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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 "ExternalResourcesPage.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui_ExternalResourcesPage.h"
@@ -9,6 +44,7 @@
#include <QKeyEvent>
#include <QMenu>
+#include <algorithm>
ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ResourceFolderModel> model, QWidget* parent)
: QMainWindow(parent), m_instance(instance), ui(new Ui::ExternalResourcesPage), m_model(model)
@@ -45,6 +81,13 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared
auto selection_model = ui->treeView->selectionModel();
connect(selection_model, &QItemSelectionModel::currentChanged, this, &ExternalResourcesPage::current);
+ auto updateExtra = [this]() {
+ if (updateExtraInfo)
+ updateExtraInfo(extraHeaderInfoString());
+ };
+ connect(selection_model, &QItemSelectionModel::selectionChanged, this, updateExtra);
+ connect(model.get(), &ResourceFolderModel::updateFinished, this, updateExtra);
+
connect(ui->filterEdit, &QLineEdit::textChanged, this, &ExternalResourcesPage::filterTextChanged);
auto viewHeader = ui->treeView->header();
@@ -263,6 +306,15 @@ bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, const
int row = sourceCurrent.row();
Resource const& resource = m_model->at(row);
ui->frame->updateWithResource(resource);
-
return true;
}
+
+QString ExternalResourcesPage::extraHeaderInfoString()
+{
+ if (ui && ui->treeView && ui->treeView->selectionModel()) {
+ auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();
+ if (auto count = std::count_if(selection.cbegin(), selection.cend(), [](auto v) { return v.column() == 0; }); count != 0)
+ return tr(" (%1 installed, %2 selected)").arg(m_model->size()).arg(count);
+ }
+ return tr(" (%1 installed)").arg(m_model->size());
+}
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h
index 906e6df7..6c0a12cb 100644
--- a/launcher/ui/pages/instance/ExternalResourcesPage.h
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.h
@@ -29,6 +29,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
virtual QString helpPage() const override = 0;
virtual bool shouldDisplay() const override = 0;
+ QString extraHeaderInfoString();
void openedImpl() override;
void closedImpl() override;
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
index 4b4c73dc..08977841 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
@@ -60,21 +60,18 @@ InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent)
m_settings = inst->settings();
ui->setupUi(this);
- accountMenu = new QMenu(this);
- // Use undocumented property... https://stackoverflow.com/questions/7121718/create-a-scrollbar-in-a-submenu-qt
- accountMenu->setStyleSheet("QMenu { menu-scrollable: 1; }");
- ui->instanceAccountSelector->setMenu(accountMenu);
+ // As the signal will (probably) not be triggered once we click edit, let's update it manually instead.
+ updateRunningStatus(m_instance->isRunning());
+ connect(m_instance, &BaseInstance::runningStatusChanged, this, &InstanceSettingsPage::updateRunningStatus);
connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked);
connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings);
connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings);
+ connect(ui->instanceAccountSelector, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &InstanceSettingsPage::changeInstanceAccount);
loadSettings();
- updateThresholds();
-}
-bool InstanceSettingsPage::shouldDisplay() const
-{
- return !m_instance->isRunning();
+
+ updateThresholds();
}
InstanceSettingsPage::~InstanceSettingsPage()
@@ -454,36 +451,17 @@ void InstanceSettingsPage::on_javaTestBtn_clicked()
void InstanceSettingsPage::updateAccountsMenu()
{
- accountMenu->clear();
-
+ ui->instanceAccountSelector->clear();
auto accounts = APPLICATION->accounts();
int accountIndex = accounts->findAccountByProfileId(m_settings->get("InstanceAccountId").toString());
- MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
-
- if (accountIndex != -1 && accounts->at(accountIndex)) {
- defaultAccount = accounts->at(accountIndex);
- }
-
- if (defaultAccount) {
- ui->instanceAccountSelector->setText(defaultAccount->profileName());
- ui->instanceAccountSelector->setIcon(getFaceForAccount(defaultAccount));
- } else {
- ui->instanceAccountSelector->setText(tr("No default account"));
- ui->instanceAccountSelector->setIcon(APPLICATION->getThemedIcon("noaccount"));
- }
for (int i = 0; i < accounts->count(); i++) {
MinecraftAccountPtr account = accounts->at(i);
- QAction* action = new QAction(account->profileName(), this);
- action->setData(i);
- action->setCheckable(true);
- if (accountIndex == i) {
- action->setChecked(true);
- }
- action->setIcon(getFaceForAccount(account));
- accountMenu->addAction(action);
- connect(action, SIGNAL(triggered(bool)), this, SLOT(changeInstanceAccount()));
+ ui->instanceAccountSelector->addItem(getFaceForAccount(account), account->profileName(), i);
+ if (i == accountIndex)
+ ui->instanceAccountSelector->setCurrentIndex(i);
}
+
}
QIcon InstanceSettingsPage::getFaceForAccount(MinecraftAccountPtr account)
@@ -495,20 +473,13 @@ QIcon InstanceSettingsPage::getFaceForAccount(MinecraftAccountPtr account)
return APPLICATION->getThemedIcon("noaccount");
}
-void InstanceSettingsPage::changeInstanceAccount()
+void InstanceSettingsPage::changeInstanceAccount(int index)
{
- QAction* sAction = (QAction*)sender();
-
- Q_ASSERT(sAction->data().type() == QVariant::Type::Int);
-
- QVariant data = sAction->data();
- int index = data.toInt();
auto accounts = APPLICATION->accounts();
- auto account = accounts->at(index);
- m_settings->set("InstanceAccountId", account->profileId());
-
- ui->instanceAccountSelector->setText(account->profileName());
- ui->instanceAccountSelector->setIcon(getFaceForAccount(account));
+ if (index != -1 && accounts->at(index) && ui->instanceAccountGroupBox->isChecked()) {
+ auto account = accounts->at(index);
+ m_settings->set("InstanceAccountId", account->profileId());
+ }
}
void InstanceSettingsPage::on_maxMemSpinBox_valueChanged(int i)
@@ -552,3 +523,8 @@ void InstanceSettingsPage::updateThresholds()
ui->labelMaxMemIcon->setPixmap(pix);
}
}
+
+void InstanceSettingsPage::updateRunningStatus(bool running)
+{
+ setEnabled(!running);
+}
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h
index cb6fbae0..0438fe3b 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.h
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.h
@@ -75,12 +75,12 @@ public:
{
return "Instance-settings";
}
- virtual bool shouldDisplay() const override;
void retranslate() override;
void updateThresholds();
private slots:
+ void updateRunningStatus(bool running);
void on_javaDetectBtn_clicked();
void on_javaTestBtn_clicked();
void on_javaBrowseBtn_clicked();
@@ -95,12 +95,11 @@ private slots:
void updateAccountsMenu();
QIcon getFaceForAccount(MinecraftAccountPtr account);
- void changeInstanceAccount();
+ void changeInstanceAccount(int index);
private:
Ui::InstanceSettingsPage *ui;
BaseInstance *m_instance;
SettingsObjectPtr m_settings;
unique_qobject_ptr<JavaCommon::TestCheck> checker;
- QMenu *accountMenu = nullptr;
};
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui
index 1b986184..8427965d 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.ui
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui
@@ -269,7 +269,7 @@
<item>
<widget class="QCheckBox" name="maximizedCheckBox">
<property name="text">
- <string>Start Minecraft maximized?</string>
+ <string>Start Minecraft maximized</string>
</property>
</widget>
</item>
@@ -341,21 +341,21 @@
<item>
<widget class="QCheckBox" name="showConsoleCheck">
<property name="text">
- <string>Show console while the game is running?</string>
+ <string>Show console while the game is running</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="autoCloseConsoleCheck">
<property name="text">
- <string>Automatically close console when the game quits?</string>
+ <string>Automatically close console when the game quits</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showConsoleErrorCheck">
<property name="text">
- <string>Show console when the game crashes?</string>
+ <string>Show console when the game crashes</string>
</property>
</widget>
</item>
@@ -636,14 +636,7 @@
</widget>
</item>
<item row="0" column="1">
- <widget class="QToolButton" name="instanceAccountSelector">
- <property name="popupMode">
- <enum>QToolButton::InstantPopup</enum>
- </property>
- <property name="toolButtonStyle">
- <enum>Qt::ToolButtonTextBesideIcon</enum>
- </property>
- </widget>
+ <widget class="QComboBox" name="instanceAccountSelector"/>
</item>
</layout>
</item>
diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp
index 4548af59..90e7d0d6 100644
--- a/launcher/ui/pages/instance/ModFolderPage.cpp
+++ b/launcher/ui/pages/instance/ModFolderPage.cpp
@@ -4,6 +4,7 @@
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
@@ -86,28 +87,20 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods);
auto check_allow_update = [this] {
- return (!m_instance || !m_instance->isRunning()) &&
- (ui->treeView->selectionModel()->hasSelection() || !m_model->empty());
+ return (!m_instance || !m_instance->isRunning()) && (ui->treeView->selectionModel()->hasSelection() || !m_model->empty());
};
- connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this, check_allow_update] {
- ui->actionUpdateItem->setEnabled(check_allow_update());
- });
+ connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
+ [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
- connect(mods.get(), &ModFolderModel::rowsInserted, this, [this, check_allow_update] {
- ui->actionUpdateItem->setEnabled(check_allow_update());
- });
+ connect(mods.get(), &ModFolderModel::rowsInserted, this,
+ [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
- connect(mods.get(), &ModFolderModel::rowsRemoved, this, [this, check_allow_update] {
- ui->actionUpdateItem->setEnabled(check_allow_update());
- });
+ connect(mods.get(), &ModFolderModel::rowsRemoved, this,
+ [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
- connect(mods.get(), &ModFolderModel::updateFinished, this, [this, check_allow_update, mods] {
- ui->actionUpdateItem->setEnabled(check_allow_update());
-
- // Prevent a weird crash when trying to open the mods page twice in a session o.O
- disconnect(mods.get(), &ModFolderModel::updateFinished, this, 0);
- });
+ connect(mods.get(), &ModFolderModel::updateFinished, this,
+ [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); });
connect(m_instance, &BaseInstance::runningStatusChanged, this, &ModFolderPage::runningStateChanged);
ModFolderPage::runningStateChanged(m_instance && m_instance->isRunning());
diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp
index afd8b292..b7537890 100644
--- a/launcher/ui/pages/modplatform/ModModel.cpp
+++ b/launcher/ui/pages/modplatform/ModModel.cpp
@@ -6,12 +6,14 @@
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
+#include "minecraft/mod/ModFolderModel.h"
#include <QMessageBox>
+#include <algorithm>
namespace ResourceDownload {
-ModModel::ModModel(BaseInstance const& base_inst, ResourceAPI* api) : ResourceModel(api), m_base_instance(base_inst) {}
+ModModel::ModModel(BaseInstance& base_inst, ResourceAPI* api) : ResourceModel(api), m_base_instance(base_inst) {}
/******** Make data requests ********/
@@ -24,7 +26,7 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments()
std::optional<std::list<Version>> versions{};
- { // Version filter
+ { // Version filter
if (!m_filter->versions.empty())
versions = m_filter->versions;
}
@@ -67,4 +69,14 @@ void ModModel::searchWithTerm(const QString& term, unsigned int sort, bool filte
refresh();
}
+bool ModModel::isPackInstalled(ModPlatform::IndexedPack::Ptr pack) const
+{
+ auto allMods = static_cast<MinecraftInstance&>(m_base_instance).loaderModList()->allMods();
+ return std::any_of(allMods.cbegin(), allMods.cend(), [pack](Mod* mod) {
+ if (auto meta = mod->metadata(); meta)
+ return meta->provider == pack->provider && meta->project_id == pack->addonId;
+ return false;
+ });
+}
+
} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h
index 5d4a7785..805d8b9a 100644
--- a/launcher/ui/pages/modplatform/ModModel.h
+++ b/launcher/ui/pages/modplatform/ModModel.h
@@ -24,7 +24,7 @@ class ModModel : public ResourceModel {
Q_OBJECT
public:
- ModModel(const BaseInstance&, ResourceAPI* api);
+ ModModel(BaseInstance&, ResourceAPI* api);
/* Ask the API for more information */
void searchWithTerm(const QString& term, unsigned int sort, bool filter_changed);
@@ -42,9 +42,10 @@ class ModModel : public ResourceModel {
protected:
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0;
+ virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const override;
protected:
- const BaseInstance& m_base_instance;
+ BaseInstance& m_base_instance;
std::shared_ptr<ModFilterWidget::Filter> m_filter = nullptr;
};
diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp
index 04be43ad..60a43128 100644
--- a/launcher/ui/pages/modplatform/ModPage.cpp
+++ b/launcher/ui/pages/modplatform/ModPage.cpp
@@ -55,8 +55,7 @@
namespace ResourceDownload {
-ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance)
- : ResourcePage(dialog, instance)
+ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance)
{
connect(m_ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
@@ -75,12 +74,10 @@ void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget)
m_filter_widget->setInstance(&static_cast<MinecraftInstance&>(m_base_instance));
m_filter = m_filter_widget->getFilter();
- connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, [&]{
- m_ui->searchButton->setStyleSheet("text-decoration: underline");
- });
- connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this, [&]{
- m_ui->searchButton->setStyleSheet("text-decoration: none");
- });
+ connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this,
+ [&] { m_ui->searchButton->setStyleSheet("text-decoration: underline"); });
+ connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this,
+ [&] { m_ui->searchButton->setStyleSheet("text-decoration: none"); });
}
/******** Callbacks to events in the UI (set up in the derived classes) ********/
@@ -92,17 +89,13 @@ void ModPage::filterMods()
void ModPage::triggerSearch()
{
- auto changed = m_filter_widget->changed();
m_filter = m_filter_widget->getFilter();
+ m_ui->packView->clearSelection();
+ m_ui->packDescription->clear();
+ m_ui->versionSelectionBox->clear();
+ updateSelectionButton();
- if (changed) {
- m_ui->packView->clearSelection();
- m_ui->packDescription->clear();
- m_ui->versionSelectionBox->clear();
- updateSelectionButton();
- }
-
- static_cast<ModModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed);
+ static_cast<ModModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), m_filter_widget->changed());
m_fetch_progress.watch(m_model->activeSearchJob().get());
}
@@ -125,11 +118,13 @@ void ModPage::updateVersionList()
QString mcVersion = packProfile->getComponentVersion("net.minecraft");
auto current_pack = getCurrentPack();
- for (int i = 0; i < current_pack.versions.size(); i++) {
- auto version = current_pack.versions[i];
+ if (!current_pack)
+ return;
+ for (int i = 0; i < current_pack->versions.size(); i++) {
+ auto version = current_pack->versions[i];
bool valid = false;
- for(auto& mcVer : m_filter->versions){
- //NOTE: Flame doesn't care about loader, so passing it changes nothing.
+ for (auto& mcVer : m_filter->versions) {
+ // NOTE: Flame doesn't care about loader, so passing it changes nothing.
if (validateVersion(version, mcVer.toString(), packProfile->getModLoaders())) {
valid = true;
break;
@@ -148,10 +143,12 @@ void ModPage::updateVersionList()
updateSelectionButton();
}
-void ModPage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
+void ModPage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack,
+ ModPlatform::IndexedVersion& version,
+ const std::shared_ptr<ResourceFolderModel> base_model)
{
bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool();
- m_parent_dialog->addResource(pack, version, is_indexed);
+ m_model->addPack(pack, version, base_model, is_indexed);
}
} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h
index 4ea55efa..5510c191 100644
--- a/launcher/ui/pages/modplatform/ModPage.h
+++ b/launcher/ui/pages/modplatform/ModPage.h
@@ -8,8 +8,8 @@
#include "modplatform/ModIndex.h"
-#include "ui/pages/modplatform/ResourcePage.h"
#include "ui/pages/modplatform/ModModel.h"
+#include "ui/pages/modplatform/ResourcePage.h"
#include "ui/widgets/ModFilterWidget.h"
namespace Ui {
@@ -25,13 +25,14 @@ class ModPage : public ResourcePage {
Q_OBJECT
public:
- template<typename T>
+ template <typename T>
static T* create(ModDownloadDialog* dialog, BaseInstance& instance)
{
auto page = new T(dialog, instance);
auto model = static_cast<ModModel*>(page->getModel());
- auto filter_widget = ModFilterWidget::create(static_cast<MinecraftInstance&>(instance).getPackProfile()->getComponentVersion("net.minecraft"), page);
+ auto filter_widget =
+ ModFilterWidget::create(static_cast<MinecraftInstance&>(instance).getPackProfile()->getComponentVersion("net.minecraft"), page);
page->setFilterWidget(filter_widget);
model->setFilter(page->getFilter());
@@ -48,9 +49,13 @@ class ModPage : public ResourcePage {
[[nodiscard]] QMap<QString, QString> urlHandlers() const override;
- void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&) override;
+ void addResourceToPage(ModPlatform::IndexedPack::Ptr,
+ ModPlatform::IndexedVersion&,
+ const std::shared_ptr<ResourceFolderModel>) override;
- virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool = 0;
+ virtual auto validateVersion(ModPlatform::IndexedVersion& ver,
+ QString mineVer,
+ std::optional<ResourceAPI::ModLoaderTypes> loaders = {}) const -> bool = 0;
[[nodiscard]] bool supportsFiltering() const override { return true; };
auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; }
diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp
index 472aa851..49405a02 100644
--- a/launcher/ui/pages/modplatform/ResourceModel.cpp
+++ b/launcher/ui/pages/modplatform/ResourceModel.cpp
@@ -6,9 +6,11 @@
#include <QCryptographicHash>
#include <QIcon>
+#include <QList>
#include <QMessageBox>
#include <QPixmapCache>
#include <QUrl>
+#include <algorithm>
#include <memory>
#include "Application.h"
@@ -65,7 +67,7 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant
return QSize(0, 58);
case Qt::UserRole: {
QVariant v;
- v.setValue(*pack);
+ v.setValue(pack);
return v;
}
// Custom data
@@ -75,6 +77,8 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant
return pack->description;
case UserDataTypes::SELECTED:
return pack->isAnyVersionSelected();
+ case UserDataTypes::INSTALLED:
+ return this->isPackInstalled(pack);
default:
break;
}
@@ -93,6 +97,7 @@ QHash<int, QByteArray> ResourceModel::roleNames() const
roles[UserDataTypes::TITLE] = "title";
roles[UserDataTypes::DESCRIPTION] = "description";
roles[UserDataTypes::SELECTED] = "selected";
+ roles[UserDataTypes::INSTALLED] = "installed";
return roles;
}
@@ -103,7 +108,7 @@ bool ResourceModel::setData(const QModelIndex& index, const QVariant& value, int
if (pos >= m_packs.size() || pos < 0 || !index.isValid())
return false;
- m_packs[pos] = std::make_shared<ModPlatform::IndexedPack>(value.value<ModPlatform::IndexedPack>());
+ m_packs[pos] = value.value<ModPlatform::IndexedPack::Ptr>();
emit dataChanged(index, index);
return true;
@@ -230,7 +235,7 @@ void ResourceModel::clearData()
void ResourceModel::runSearchJob(Task::Ptr ptr)
{
- m_current_search_job.reset(ptr); // clean up first
+ m_current_search_job.reset(ptr); // clean up first
m_current_search_job->start();
}
void ResourceModel::runInfoJob(Task::Ptr ptr)
@@ -336,7 +341,15 @@ void ResourceModel::searchRequestSucceeded(QJsonDocument& doc)
ModPlatform::IndexedPack::Ptr pack = std::make_shared<ModPlatform::IndexedPack>();
try {
loadIndexedPack(*pack, packObj);
- newList.append(pack);
+ if (auto sel = std::find_if(m_selected.begin(), m_selected.end(),
+ [&pack](const DownloadTaskPtr i) {
+ const auto ipack = i->getPack();
+ return ipack->provider == pack->provider && ipack->addonId == pack->addonId;
+ });
+ sel != m_selected.end()) {
+ newList.append(sel->get()->getPack());
+ } else
+ newList.append(pack);
} catch (const JSONValidationError& e) {
qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause();
continue;
@@ -390,15 +403,15 @@ void ResourceModel::searchRequestAborted()
void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
{
- auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
+ auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
// Check if the index is still valid for this resource or not
- if (pack.addonId != current_pack.addonId)
+ if (pack.addonId != current_pack->addonId)
return;
try {
auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
- loadIndexedPackVersions(current_pack, arr);
+ loadIndexedPackVersions(*current_pack, arr);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause();
@@ -417,15 +430,15 @@ void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::Ind
void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index)
{
- auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack>();
+ auto current_pack = data(index, Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
// Check if the index is still valid for this resource or not
- if (pack.addonId != current_pack.addonId)
+ if (pack.addonId != current_pack->addonId)
return;
try {
auto obj = Json::requireObject(doc);
- loadExtraPackInfo(current_pack, obj);
+ loadExtraPackInfo(*current_pack, obj);
} catch (const JSONValidationError& e) {
qDebug() << doc;
qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause();
@@ -442,4 +455,39 @@ void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::Indexe
emit projectInfoUpdated();
}
+void ResourceModel::addPack(ModPlatform::IndexedPack::Ptr pack,
+ ModPlatform::IndexedVersion& version,
+ const std::shared_ptr<ResourceFolderModel> packs,
+ bool is_indexed,
+ QString custom_target_folder)
+{
+ version.is_currently_selected = true;
+ m_selected.append(makeShared<ResourceDownloadTask>(pack, version, packs, is_indexed, custom_target_folder));
+}
+
+void ResourceModel::removePack(const QString& rem)
+{
+ auto pred = [&rem](const DownloadTaskPtr i) { return rem == i->getName(); };
+#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0)
+ m_selected.removeIf(pred);
+#else
+ {
+ for (auto it = m_selected.begin(); it != m_selected.end();)
+ if (pred(*it))
+ it = m_selected.erase(it);
+ else
+ ++it;
+ }
+#endif
+ auto pack = std::find_if(m_packs.begin(), m_packs.end(), [&rem](const ModPlatform::IndexedPack::Ptr i) { return rem == i->name; });
+ if (pack == m_packs.end()) { // ignore it if is not in the current search
+ return;
+ }
+ if (!pack->get()->versionsLoaded) {
+ return;
+ }
+ for (auto& ver : pack->get()->versions)
+ ver.is_currently_selected = false;
+}
+
} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h
index 1ec42cda..6533d9c6 100644
--- a/launcher/ui/pages/modplatform/ResourceModel.h
+++ b/launcher/ui/pages/modplatform/ResourceModel.h
@@ -10,6 +10,7 @@
#include "QObjectPtr.h"
+#include "ResourceDownloadTask.h"
#include "modplatform/ResourceAPI.h"
#include "tasks/ConcurrentTask.h"
@@ -29,6 +30,8 @@ class ResourceModel : public QAbstractListModel {
Q_PROPERTY(QString search_term MEMBER m_search_term WRITE setSearchTerm)
public:
+ using DownloadTaskPtr = shared_qobject_ptr<ResourceDownloadTask>;
+
ResourceModel(ResourceAPI* api);
~ResourceModel() override;
@@ -80,6 +83,14 @@ class ResourceModel : public QAbstractListModel {
/** Gets the icon at the URL for the given index. If it's not fetched yet, fetch it and update when fisinhed. */
std::optional<QIcon> getIcon(QModelIndex&, const QUrl&);
+ void addPack(ModPlatform::IndexedPack::Ptr pack,
+ ModPlatform::IndexedVersion& version,
+ const std::shared_ptr<ResourceFolderModel> packs,
+ bool is_indexed = false,
+ QString custom_target_folder = {});
+ void removePack(const QString& rem);
+ QList<DownloadTaskPtr> selectedPacks() { return m_selected; }
+
protected:
/** Resets the model's data. */
void clearData();
@@ -105,6 +116,8 @@ class ResourceModel : public QAbstractListModel {
virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&);
virtual void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&);
+ virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const { return false; }
+
protected:
/* Basic search parameters */
enum class SearchState { None, CanFetchMore, ResetRequested, Finished } m_search_state = SearchState::None;
@@ -124,6 +137,7 @@ class ResourceModel : public QAbstractListModel {
QSet<QUrl> m_failed_icon_actions;
QList<ModPlatform::IndexedPack::Ptr> m_packs;
+ QList<DownloadTaskPtr> m_selected;
// HACK: We need this to prevent callbacks from calling the model after it has already been deleted.
// This leaks a tiny bit of memory per time the user has opened a resource dialog. How to make this better?
diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp
index f75bb886..aab2ee89 100644
--- a/launcher/ui/pages/modplatform/ResourcePage.cpp
+++ b/launcher/ui/pages/modplatform/ResourcePage.cpp
@@ -37,6 +37,7 @@
*/
#include "ResourcePage.h"
+#include "modplatform/ModIndex.h"
#include "ui_ResourcePage.h"
#include <QDesktopServices>
@@ -158,31 +159,35 @@ void ResourcePage::addSortings()
m_ui->sortByBox->addItem(sorting.readable_name, QVariant(sorting.index));
}
-bool ResourcePage::setCurrentPack(ModPlatform::IndexedPack pack)
+bool ResourcePage::setCurrentPack(ModPlatform::IndexedPack::Ptr pack)
{
QVariant v;
v.setValue(pack);
return m_model->setData(m_ui->packView->currentIndex(), v, Qt::UserRole);
}
-ModPlatform::IndexedPack ResourcePage::getCurrentPack() const
+ModPlatform::IndexedPack::Ptr ResourcePage::getCurrentPack() const
{
- return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value<ModPlatform::IndexedPack>();
+ return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value<ModPlatform::IndexedPack::Ptr>();
}
void ResourcePage::updateUi()
{
auto current_pack = getCurrentPack();
-
+ if (!current_pack) {
+ m_ui->packDescription->setHtml({});
+ m_ui->packDescription->flush();
+ return;
+ }
QString text = "";
- QString name = current_pack.name;
+ QString name = current_pack->name;
- if (current_pack.websiteUrl.isEmpty())
+ if (current_pack->websiteUrl.isEmpty())
text = name;
else
- text = "<a href=\"" + current_pack.websiteUrl + "\">" + name + "</a>";
+ text = "<a href=\"" + current_pack->websiteUrl + "\">" + name + "</a>";
- if (!current_pack.authors.empty()) {
+ if (!current_pack->authors.empty()) {
auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString {
if (author.url.isEmpty()) {
return author.name;
@@ -190,44 +195,44 @@ void ResourcePage::updateUi()
return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
};
QStringList authorStrs;
- for (auto& author : current_pack.authors) {
+ for (auto& author : current_pack->authors) {
authorStrs.push_back(authorToStr(author));
}
text += "<br>" + tr(" by ") + authorStrs.join(", ");
}
- if (current_pack.extraDataLoaded) {
- if (!current_pack.extraData.donate.isEmpty()) {
+ if (current_pack->extraDataLoaded) {
+ if (!current_pack->extraData.donate.isEmpty()) {
text += "<br><br>" + tr("Donate information: ");
auto donateToStr = [](ModPlatform::DonationData& donate) -> QString {
return QString("<a href=\"%1\">%2</a>").arg(donate.url, donate.platform);
};
QStringList donates;
- for (auto& donate : current_pack.extraData.donate) {
+ for (auto& donate : current_pack->extraData.donate) {
donates.append(donateToStr(donate));
}
text += donates.join(", ");
}
- if (!current_pack.extraData.issuesUrl.isEmpty() || !current_pack.extraData.sourceUrl.isEmpty() ||
- !current_pack.extraData.wikiUrl.isEmpty() || !current_pack.extraData.discordUrl.isEmpty()) {
+ if (!current_pack->extraData.issuesUrl.isEmpty() || !current_pack->extraData.sourceUrl.isEmpty() ||
+ !current_pack->extraData.wikiUrl.isEmpty() || !current_pack->extraData.discordUrl.isEmpty()) {
text += "<br><br>" + tr("External links:") + "<br>";
}
- if (!current_pack.extraData.issuesUrl.isEmpty())
- text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current_pack.extraData.issuesUrl) + "<br>";
- if (!current_pack.extraData.wikiUrl.isEmpty())
- text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current_pack.extraData.wikiUrl) + "<br>";
- if (!current_pack.extraData.sourceUrl.isEmpty())
- text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current_pack.extraData.sourceUrl) + "<br>";
- if (!current_pack.extraData.discordUrl.isEmpty())
- text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current_pack.extraData.discordUrl) + "<br>";
+ if (!current_pack->extraData.issuesUrl.isEmpty())
+ text += "- " + tr("Issues: <a href=%1>%1</a>").arg(current_pack->extraData.issuesUrl) + "<br>";
+ if (!current_pack->extraData.wikiUrl.isEmpty())
+ text += "- " + tr("Wiki: <a href=%1>%1</a>").arg(current_pack->extraData.wikiUrl) + "<br>";
+ if (!current_pack->extraData.sourceUrl.isEmpty())
+ text += "- " + tr("Source code: <a href=%1>%1</a>").arg(current_pack->extraData.sourceUrl) + "<br>";
+ if (!current_pack->extraData.discordUrl.isEmpty())
+ text += "- " + tr("Discord: <a href=%1>%1</a>").arg(current_pack->extraData.discordUrl) + "<br>";
}
text += "<hr>";
m_ui->packDescription->setHtml(
- text + (current_pack.extraData.body.isEmpty() ? current_pack.description : markdownToHTML(current_pack.extraData.body)));
+ text + (current_pack->extraData.body.isEmpty() ? current_pack->description : markdownToHTML(current_pack->extraData.body)));
m_ui->packDescription->flush();
}
@@ -239,10 +244,13 @@ void ResourcePage::updateSelectionButton()
}
m_ui->resourceSelectionButton->setEnabled(true);
- if (!getCurrentPack().isVersionSelected(m_selected_version_index)) {
- m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString()));
+ if (auto current_pack = getCurrentPack(); current_pack) {
+ if (!current_pack->isVersionSelected(m_selected_version_index))
+ m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString()));
+ else
+ m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString()));
} else {
- m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString()));
+ qWarning() << "Tried to update the selected button but there is not a pack selected";
}
}
@@ -254,13 +262,14 @@ void ResourcePage::updateVersionList()
m_ui->versionSelectionBox->clear();
m_ui->versionSelectionBox->blockSignals(false);
- for (int i = 0; i < current_pack.versions.size(); i++) {
- auto& version = current_pack.versions[i];
- if (optedOut(version))
- continue;
+ if (current_pack)
+ for (int i = 0; i < current_pack->versions.size(); i++) {
+ auto& version = current_pack->versions[i];
+ if (optedOut(version))
+ continue;
- m_ui->versionSelectionBox->addItem(current_pack.versions[i].version, QVariant(i));
- }
+ m_ui->versionSelectionBox->addItem(current_pack->versions[i].version, QVariant(i));
+ }
if (m_ui->versionSelectionBox->count() == 0) {
m_ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1));
@@ -279,7 +288,7 @@ void ResourcePage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
auto current_pack = getCurrentPack();
bool request_load = false;
- if (!current_pack.versionsLoaded) {
+ if (!current_pack || !current_pack->versionsLoaded) {
m_ui->resourceSelectionButton->setText(tr("Loading versions..."));
m_ui->resourceSelectionButton->setEnabled(false);
@@ -288,7 +297,7 @@ void ResourcePage::onSelectionChanged(QModelIndex curr, QModelIndex prev)
updateVersionList();
}
- if (!current_pack.extraDataLoaded)
+ if (current_pack && !current_pack->extraDataLoaded)
request_load = true;
if (request_load)
@@ -308,14 +317,26 @@ void ResourcePage::onVersionSelectionChanged(QString data)
updateSelectionButton();
}
-void ResourcePage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
+void ResourcePage::addResourceToDialog(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version)
{
m_parent_dialog->addResource(pack, version);
}
-void ResourcePage::removeResourceFromDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
+void ResourcePage::removeResourceFromDialog(const QString& pack_name)
+{
+ m_parent_dialog->removeResource(pack_name);
+}
+
+void ResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack,
+ ModPlatform::IndexedVersion& ver,
+ const std::shared_ptr<ResourceFolderModel> base_model)
+{
+ m_model->addPack(pack, ver, base_model);
+}
+
+void ResourcePage::removeResourceFromPage(const QString& name)
{
- m_parent_dialog->removeResource(pack, version);
+ m_model->removePack(name);
}
void ResourcePage::onResourceSelected()
@@ -324,12 +345,12 @@ void ResourcePage::onResourceSelected()
return;
auto current_pack = getCurrentPack();
- if (!current_pack.versionsLoaded)
+ if (!current_pack || !current_pack->versionsLoaded)
return;
- auto& version = current_pack.versions[m_selected_version_index];
+ auto& version = current_pack->versions[m_selected_version_index];
if (version.is_currently_selected)
- removeResourceFromDialog(current_pack, version);
+ removeResourceFromDialog(current_pack->name);
else
addResourceToDialog(current_pack, version);
@@ -340,7 +361,7 @@ void ResourcePage::onResourceSelected()
updateSelectionButton();
/* Force redraw on the resource list when the selection changes */
- m_ui->packView->adjustSize();
+ m_ui->packView->repaint();
}
void ResourcePage::openUrl(const QUrl& url)
@@ -370,7 +391,7 @@ void ResourcePage::openUrl(const QUrl& url)
const QString slug = match.captured(1);
// ensure the user isn't opening the same mod
- if (slug != getCurrentPack().slug) {
+ if (auto current_pack = getCurrentPack(); current_pack && slug != current_pack->slug) {
m_parent_dialog->selectPage(page);
auto newPage = m_parent_dialog->getSelectedPage();
diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h
index 1896d53e..b4a87f57 100644
--- a/launcher/ui/pages/modplatform/ResourcePage.h
+++ b/launcher/ui/pages/modplatform/ResourcePage.h
@@ -7,10 +7,12 @@
#include <QTimer>
#include <QWidget>
+#include "ResourceDownloadTask.h"
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "ui/pages/BasePage.h"
+#include "ui/pages/modplatform/ResourceModel.h"
#include "ui/widgets/ProgressWidget.h"
namespace Ui {
@@ -27,6 +29,7 @@ class ResourceModel;
class ResourcePage : public QWidget, public BasePage {
Q_OBJECT
public:
+ using DownloadTaskPtr = shared_qobject_ptr<ResourceDownloadTask>;
~ResourcePage() override;
/* Affects what the user sees */
@@ -57,8 +60,8 @@ class ResourcePage : public QWidget, public BasePage {
/** Programatically set the term in the search bar. */
void setSearchTerm(QString);
- [[nodiscard]] bool setCurrentPack(ModPlatform::IndexedPack);
- [[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack;
+ [[nodiscard]] bool setCurrentPack(ModPlatform::IndexedPack::Ptr);
+ [[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack::Ptr;
[[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parent_dialog; }
[[nodiscard]] auto getModel() const -> ResourceModel* { return m_model; }
@@ -72,12 +75,17 @@ class ResourcePage : public QWidget, public BasePage {
virtual void updateSelectionButton();
virtual void updateVersionList();
- virtual void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
- virtual void removeResourceFromDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&);
+ void addResourceToDialog(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&);
+ void removeResourceFromDialog(const QString& pack_name);
+ virtual void removeResourceFromPage(const QString& name);
+ virtual void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, const std::shared_ptr<ResourceFolderModel>);
+
+ QList<DownloadTaskPtr> selectedPacks() { return m_model->selectedPacks(); }
+ bool hasSelectedPacks() { return !(m_model->selectedPacks().isEmpty()); }
protected slots:
virtual void triggerSearch() {}
-
+
void onSelectionChanged(QModelIndex first, QModelIndex second);
void onVersionSelectionChanged(QString data);
void onResourceSelected();
diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.cpp b/launcher/ui/pages/modplatform/ShaderPackPage.cpp
index 251c07e7..fbf94e84 100644
--- a/launcher/ui/pages/modplatform/ShaderPackPage.cpp
+++ b/launcher/ui/pages/modplatform/ShaderPackPage.cpp
@@ -13,8 +13,7 @@
namespace ResourceDownload {
-ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance)
- : ResourcePage(dialog, instance)
+ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance)
{
connect(m_ui->searchButton, &QPushButton::clicked, this, &ShaderPackResourcePage::triggerSearch);
connect(m_ui->packView, &QListView::doubleClicked, this, &ShaderPackResourcePage::onResourceSelected);
@@ -38,17 +37,20 @@ QMap<QString, QString> ShaderPackResourcePage::urlHandlers() const
{
QMap<QString, QString> map;
map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/shaders\\/([^\\/]+)\\/?"), "modrinth");
- map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/customization\\/([^\\/]+)\\/?"), "curseforge");
+ map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/customization\\/([^\\/]+)\\/?"),
+ "curseforge");
map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge");
return map;
}
-void ShaderPackResourcePage::addResourceToDialog(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& version)
+void ShaderPackResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack,
+ ModPlatform::IndexedVersion& version,
+ const std::shared_ptr<ResourceFolderModel> base_model)
{
+ QString custom_target_folder;
if (version.loaders.contains(QStringLiteral("canvas")))
- version.custom_target_folder = QStringLiteral("resourcepacks");
-
- m_parent_dialog->addResource(pack, version);
+ custom_target_folder = QStringLiteral("resourcepacks");
+ m_model->addPack(pack, version, base_model, false, custom_target_folder);
}
} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.h b/launcher/ui/pages/modplatform/ShaderPackPage.h
index 9039c4d9..fcf6d4a7 100644
--- a/launcher/ui/pages/modplatform/ShaderPackPage.h
+++ b/launcher/ui/pages/modplatform/ShaderPackPage.h
@@ -38,7 +38,9 @@ class ShaderPackResourcePage : public ResourcePage {
[[nodiscard]] bool supportsFiltering() const override { return false; };
- void addResourceToDialog(ModPlatform::IndexedPack&, ModPlatform::IndexedVersion&) override;
+ void addResourceToPage(ModPlatform::IndexedPack::Ptr,
+ ModPlatform::IndexedVersion&,
+ const std::shared_ptr<ResourceFolderModel>) override;
[[nodiscard]] QMap<QString, QString> urlHandlers() const override;
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp
index f5f50cae..3d2d568a 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp
@@ -68,7 +68,7 @@ QString AtlUserInteractionSupportImpl::chooseVersion(Meta::VersionList::Ptr vlis
// select recommended build
for (int i = 0; i < vlist->versions().size(); i++) {
auto version = vlist->versions().at(i);
- auto reqs = version->requires();
+ auto reqs = version->requiredSet();
// filter by minecraft version, if the loader depends on a certain version.
if (minecraftVersion != nullptr) {
diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp
index 5961ea02..d9d5ef5b 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp
@@ -60,6 +60,8 @@ QVariant ListModel::data(const QModelIndex& index, int role) const
return pack.description;
case UserDataTypes::SELECTED:
return false;
+ case UserDataTypes::INSTALLED:
+ return false;
default:
break;
}
diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp
index e3d0bc14..667a52d0 100644
--- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp
@@ -11,7 +11,7 @@
namespace ResourceDownload {
-FlameModModel::FlameModModel(BaseInstance const& base) : ModModel(base, new FlameAPI) {}
+FlameModModel::FlameModModel(BaseInstance& base) : ModModel(base, new FlameAPI) {}
void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h
index 0252ac40..221c8f7a 100644
--- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h
+++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h
@@ -14,7 +14,7 @@ class FlameModModel : public ModModel {
Q_OBJECT
public:
- FlameModModel(const BaseInstance&);
+ FlameModModel(BaseInstance&);
~FlameModModel() override = default;
private:
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
index 346a00b0..55d287b0 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
@@ -106,6 +106,8 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian
return pack.description;
case UserDataTypes::SELECTED:
return false;
+ case UserDataTypes::INSTALLED:
+ return false;
default:
break;
}
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
index f5d1cc28..7f857485 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
@@ -25,7 +25,7 @@
namespace ResourceDownload {
-ModrinthModModel::ModrinthModModel(BaseInstance const& base) : ModModel(base, new ModrinthAPI) {}
+ModrinthModModel::ModrinthModModel(BaseInstance& base) : ModModel(base, new ModrinthAPI) {}
void ModrinthModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
{
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h
index b351b19b..66461807 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h
@@ -30,7 +30,7 @@ class ModrinthModModel : public ModModel {
Q_OBJECT
public:
- ModrinthModModel(const BaseInstance&);
+ ModrinthModModel(BaseInstance&);
~ModrinthModModel() override = default;
private:
diff --git a/launcher/ui/themes/SystemTheme.cpp b/launcher/ui/themes/SystemTheme.cpp
index a95bc875..3a746d02 100644
--- a/launcher/ui/themes/SystemTheme.cpp
+++ b/launcher/ui/themes/SystemTheme.cpp
@@ -43,7 +43,7 @@ SystemTheme::SystemTheme()
{
themeDebugLog() << "Determining System Theme...";
const auto& style = QApplication::style();
- systemPalette = style->standardPalette();
+ systemPalette = QApplication::palette();
QString lowerThemeName = style->objectName();
themeDebugLog() << "System theme seems to be:" << lowerThemeName;
QStringList styles = QStyleFactory::keys();
diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp
index 15994319..c94fdd8d 100644
--- a/launcher/ui/widgets/JavaSettingsWidget.cpp
+++ b/launcher/ui/widgets/JavaSettingsWidget.cpp
@@ -46,7 +46,7 @@ void JavaSettingsWidget::setupUi()
m_verticalLayout = new QVBoxLayout(this);
m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
- m_versionWidget = new VersionSelectWidget(this);
+ m_versionWidget = new VersionSelectWidget(true, this);
m_verticalLayout->addWidget(m_versionWidget);
m_horizontalLayout = new QHBoxLayout();
diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp
index b9b17b42..34df42ec 100644
--- a/launcher/ui/widgets/PageContainer.cpp
+++ b/launcher/ui/widgets/PageContainer.cpp
@@ -93,6 +93,10 @@ PageContainer::PageContainer(BasePageProvider *pageProvider, QString defaultId,
page->listIndex = counter;
page->setParentContainer(this);
counter++;
+ page->updateExtraInfo = [this](QString info) {
+ if (m_currentPage)
+ m_header->setText(m_currentPage->displayName() + info);
+ };
}
m_model->setPages(pages);
@@ -137,6 +141,11 @@ BasePage* PageContainer::getPage(QString pageId)
return m_model->findPageEntryById(pageId);
}
+const QList<BasePage*> PageContainer::getPages() const
+{
+ return m_model->pages();
+}
+
void PageContainer::refreshContainer()
{
m_proxyModel->invalidate();
diff --git a/launcher/ui/widgets/PageContainer.h b/launcher/ui/widgets/PageContainer.h
index 97e294dc..ad74d43a 100644
--- a/launcher/ui/widgets/PageContainer.h
+++ b/launcher/ui/widgets/PageContainer.h
@@ -80,6 +80,7 @@ public:
virtual bool selectPage(QString pageId) override;
BasePage* getPage(QString pageId) override;
+ const QList<BasePage*> getPages() const;
void refreshContainer() override;
virtual void setParentContainer(BasePageContainer * container)
diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp
index d1ff9dbc..0085d6b2 100644
--- a/launcher/ui/widgets/ProjectItem.cpp
+++ b/launcher/ui/widgets/ProjectItem.cpp
@@ -64,6 +64,17 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o
font.setBold(true);
font.setUnderline(true);
}
+ if (index.data(UserDataTypes::INSTALLED).toBool()) {
+ auto hRect = opt.rect;
+ hRect.setX(hRect.x() + 1);
+ hRect.setY(hRect.y() + 1);
+ hRect.setHeight(hRect.height() - 2);
+ hRect.setWidth(hRect.width() - 2);
+ // Set nice font
+ font.setItalic(true);
+ font.setOverline(true);
+ painter->drawRect(hRect);
+ }
font.setPointSize(font.pointSize() + 2);
painter->setFont(font);
diff --git a/launcher/ui/widgets/ProjectItem.h b/launcher/ui/widgets/ProjectItem.h
index f668edf6..196055ea 100644
--- a/launcher/ui/widgets/ProjectItem.h
+++ b/launcher/ui/widgets/ProjectItem.h
@@ -6,7 +6,8 @@
enum UserDataTypes {
TITLE = 257, // QString
DESCRIPTION = 258, // QString
- SELECTED = 259 // bool
+ SELECTED = 259, // bool
+ INSTALLED = 260 // bool
};
/** This is an item delegate composed of:
diff --git a/launcher/ui/widgets/VersionListView.cpp b/launcher/ui/widgets/VersionListView.cpp
index 0e126c65..06e58d22 100644
--- a/launcher/ui/widgets/VersionListView.cpp
+++ b/launcher/ui/widgets/VersionListView.cpp
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
- * PolyMC - Minecraft Launcher
+ * 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
@@ -125,14 +126,9 @@ void VersionListView::paintEvent(QPaintEvent *event)
QString VersionListView::currentEmptyString() const
{
- if(m_itemCount) {
- return QString();
- }
switch(m_emptyMode)
{
default:
- case VersionListView::Empty:
- return QString();
case VersionListView::String:
return m_emptyString;
case VersionListView::ErrorString:
diff --git a/launcher/ui/widgets/VersionSelectWidget.cpp b/launcher/ui/widgets/VersionSelectWidget.cpp
index 404860d9..a956ddb3 100644
--- a/launcher/ui/widgets/VersionSelectWidget.cpp
+++ b/launcher/ui/widgets/VersionSelectWidget.cpp
@@ -1,15 +1,20 @@
#include "VersionSelectWidget.h"
+#include <QApplication>
+#include <QEvent>
+#include <QHeaderView>
+#include <QKeyEvent>
#include <QProgressBar>
#include <QVBoxLayout>
-#include <QHeaderView>
#include "VersionProxyModel.h"
#include "ui/dialogs/CustomMessageBox.h"
-VersionSelectWidget::VersionSelectWidget(QWidget* parent)
- : QWidget(parent)
+VersionSelectWidget::VersionSelectWidget(QWidget* parent) : VersionSelectWidget(false, parent) {}
+
+VersionSelectWidget::VersionSelectWidget(bool focusSearch, QWidget* parent)
+ : QWidget(parent), focusSearch(focusSearch)
{
setObjectName(QStringLiteral("VersionSelectWidget"));
verticalLayout = new QVBoxLayout(this);
@@ -30,6 +35,21 @@ VersionSelectWidget::VersionSelectWidget(QWidget* parent)
listView->setModel(m_proxyModel);
verticalLayout->addWidget(listView);
+ search = new QLineEdit(this);
+ search->setPlaceholderText(tr("Search"));
+ search->setClearButtonEnabled(true);
+ verticalLayout->addWidget(search);
+ connect(search, &QLineEdit::textEdited, [this](const QString& value) {
+ m_proxyModel->setSearch(value);
+ if (!value.isEmpty() || !listView->selectionModel()->hasSelection()) {
+ const QModelIndex first = listView->model()->index(0, 0);
+ listView->selectionModel()->setCurrentIndex(first, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
+ listView->scrollToTop();
+ } else
+ listView->scrollTo(listView->selectionModel()->currentIndex(), QAbstractItemView::PositionAtCenter);
+ });
+ search->installEventFilter(this);
+
sneakyProgressBar = new QProgressBar(this);
sneakyProgressBar->setObjectName(QStringLiteral("sneakyProgressBar"));
sneakyProgressBar->setFormat(QStringLiteral("%p%"));
@@ -72,6 +92,23 @@ void VersionSelectWidget::setResizeOn(int column)
listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch);
}
+bool VersionSelectWidget::eventFilter(QObject *watched, QEvent *event) {
+ if (watched == search && event->type() == QEvent::KeyPress) {
+ const QKeyEvent* keyEvent = (QKeyEvent*)event;
+ const bool up = keyEvent->key() == Qt::Key_Up;
+ const bool down = keyEvent->key() == Qt::Key_Down;
+ if (up || down) {
+ const QModelIndex index = listView->model()->index(listView->currentIndex().row() + (up ? -1 : 1), 0);
+ if (index.row() >= 0 && index.row() < listView->model()->rowCount()) {
+ listView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
+ return true;
+ }
+ }
+ }
+
+ return QObject::eventFilter(watched, event);
+}
+
void VersionSelectWidget::initialize(BaseVersionList *vlist)
{
m_vlist = vlist;
@@ -79,6 +116,9 @@ void VersionSelectWidget::initialize(BaseVersionList *vlist)
listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch);
+ if (focusSearch)
+ search->setFocus();
+
if (!m_vlist->isLoaded())
{
loadList();
diff --git a/launcher/ui/widgets/VersionSelectWidget.h b/launcher/ui/widgets/VersionSelectWidget.h
index e75efc6f..be4ba768 100644
--- a/launcher/ui/widgets/VersionSelectWidget.h
+++ b/launcher/ui/widgets/VersionSelectWidget.h
@@ -1,22 +1,43 @@
-/* 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");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * 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.
*
- * 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
- * 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.
+ * 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 <QWidget>
#include <QSortFilterProxyModel>
+#include <QLineEdit>
#include "BaseVersionList.h"
#include "VersionListView.h"
@@ -30,7 +51,8 @@ class VersionSelectWidget: public QWidget
{
Q_OBJECT
public:
- explicit VersionSelectWidget(QWidget *parent = 0);
+ explicit VersionSelectWidget(QWidget *parent);
+ explicit VersionSelectWidget(bool focusSearch = false, QWidget *parent = 0);
~VersionSelectWidget();
//! loads the list if needed.
@@ -52,6 +74,7 @@ public:
void setEmptyErrorString(QString emptyErrorString);
void setEmptyMode(VersionListView::EmptyMode mode);
void setResizeOn(int column);
+ bool eventFilter(QObject* watched, QEvent* event) override;
signals:
void selectedVersionChanged(BaseVersion::Ptr version);
@@ -75,9 +98,10 @@ private:
int resizeOnColumn = 0;
Task * loadTask;
bool preselectedAlready = false;
+ bool focusSearch;
-private:
QVBoxLayout *verticalLayout = nullptr;
VersionListView *listView = nullptr;
+ QLineEdit *search;
QProgressBar *sneakyProgressBar = nullptr;
};