From 6961a39cd20a63116bb562d61472c31f28ea8738 Mon Sep 17 00:00:00 2001 From: txtsd Date: Fri, 4 Nov 2022 11:58:58 +0530 Subject: [PATCH 01/59] feat: Assign java max mem based on system RAM If the system has <6GB RAM, it uses (system RAM / 1.5) If the system has >=6GB, it uses 4GB Signed-off-by: txtsd --- launcher/Application.cpp | 16 +++++++++++++++- launcher/Application.h | 2 ++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 5772d7cad..c3c76854c 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -566,7 +566,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Memory m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, 512); - m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, 4096); + m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, suitableMaxMem()); m_settings->registerSetting("PermGen", 128); // Java Settings @@ -1633,3 +1633,17 @@ QString Application::getUserAgentUncached() return BuildConfig.USER_AGENT_UNCACHED; } + +int Application::suitableMaxMem() +{ + float totalRAM = (float)Sys::getSystemRam() / (float)Sys::mebibyte; + int maxMemoryAlloc; + + // If totalRAM < 6GB, use (totalRAM / 1.5), else 4GB + if (totalRAM < (4096 * 1.5)) + maxMemoryAlloc = (int) (totalRAM / 1.5); + else + maxMemoryAlloc = 4096; + + return maxMemoryAlloc; +} diff --git a/launcher/Application.h b/launcher/Application.h index 8fa0ab10e..280c842fc 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -198,6 +198,8 @@ public: void ShowGlobalSettings(class QWidget * parent, QString open_page = QString()); + int suitableMaxMem(); + signals: void updateAllowedChanged(bool status); void globalSettingsAboutToOpen(); From e7e56eb1e397a528df91f9ce99f738c49bde363c Mon Sep 17 00:00:00 2001 From: Marcelo Hernandez Date: Sat, 22 Oct 2022 14:50:32 -0400 Subject: [PATCH 02/59] add more options to copy instance dialog - Copy game options, copy resource packs, copy shaders, copy servers, and copy mods - Also made a new InstanceCopyPrefs struct to store those options rather than passing 7 different booleans into InstanceCopyTask's constructor Signed-off-by: Marcelo Hernandez --- launcher/CMakeLists.txt | 1 + launcher/InstanceCopyPrefs.h | 18 +++++ launcher/InstanceCopyTask.cpp | 62 +++++++++++++-- launcher/InstanceCopyTask.h | 20 +++-- launcher/ui/MainWindow.cpp | 12 ++- launcher/ui/dialogs/CopyInstanceDialog.cpp | 90 ++++++++++++++++++++++ launcher/ui/dialogs/CopyInstanceDialog.h | 15 ++++ launcher/ui/dialogs/CopyInstanceDialog.ui | 58 ++++++++++++-- 8 files changed, 254 insertions(+), 22 deletions(-) create mode 100644 launcher/InstanceCopyPrefs.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 79ac49c76..77440cca9 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -31,6 +31,7 @@ set(CORE_SOURCES # Basic instance manipulation tasks (derived from InstanceTask) InstanceCreationTask.h InstanceCreationTask.cpp + InstanceCopyPrefs.h InstanceCopyTask.h InstanceCopyTask.cpp InstanceImportTask.h diff --git a/launcher/InstanceCopyPrefs.h b/launcher/InstanceCopyPrefs.h new file mode 100644 index 000000000..ac2feab8a --- /dev/null +++ b/launcher/InstanceCopyPrefs.h @@ -0,0 +1,18 @@ +// +// Created by marcelohdez on 10/22/22. +// + +#ifndef LAUNCHER_INSTANCECOPYPREFS_H +#define LAUNCHER_INSTANCECOPYPREFS_H + +struct InstanceCopyPrefs { + bool copySaves; + bool keepPlaytime; + bool copyGameOptions; + bool copyResourcePacks; + bool copyShaderPacks; + bool copyServers; + bool copyMods; +}; + +#endif // LAUNCHER_INSTANCECOPYPREFS_H diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index b1e338844..360f6cfad 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -5,18 +5,66 @@ #include "pathmatcher/RegexpMatcher.h" #include -InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime) +InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, InstanceCopyPrefs prefs) { m_origInstance = origInstance; - m_keepPlaytime = keepPlaytime; + m_keepPlaytime = prefs.keepPlaytime; + QString filter; - if(!copySaves) + if(!prefs.copySaves) { - // FIXME: get this from the original instance type... - auto matcherReal = new RegexpMatcher("[.]?minecraft/saves"); - matcherReal->caseSensitive(false); - m_matcher.reset(matcherReal); + appendToFilter(filter, "saves"); } + + if(!prefs.copyGameOptions) { + appendToFilter(filter, "options.txt"); + } + + if(!prefs.copyResourcePacks) + { + appendToFilter(filter, "resourcepacks"); + appendToFilter(filter, "texturepacks"); + } + + if(!prefs.copyShaderPacks) + { + appendToFilter(filter, "shaderpacks"); + } + + if(!prefs.copyServers) + { + appendToFilter(filter, "servers.dat"); + appendToFilter(filter, "servers.dat_old"); + appendToFilter(filter, "server-resource-packs"); + } + + if(!prefs.copyMods) + { + appendToFilter(filter, "coremods"); + appendToFilter(filter, "mods"); + appendToFilter(filter, "config"); + } + + if (!filter.isEmpty()) + { + resetFromMatcher(filter); + } +} + +void InstanceCopyTask::appendToFilter(QString& filter, const QString &append) +{ + if (!filter.isEmpty()) + filter.append('|'); // OR regex + + filter.append("[.]?minecraft/" + append); +} + +void InstanceCopyTask::resetFromMatcher(const QString& regexp) +{ + // FIXME: get this from the original instance type... + auto matcherReal = new RegexpMatcher(regexp); + matcherReal->caseSensitive(false); + m_matcher.reset(matcherReal); } void InstanceCopyTask::executeTask() diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h index 829017326..d66bec55b 100644 --- a/launcher/InstanceCopyTask.h +++ b/launcher/InstanceCopyTask.h @@ -1,20 +1,21 @@ #pragma once -#include "tasks/Task.h" -#include "net/NetJob.h" -#include #include #include -#include "settings/SettingsObject.h" -#include "BaseVersion.h" +#include #include "BaseInstance.h" +#include "BaseVersion.h" +#include "InstanceCopyPrefs.h" #include "InstanceTask.h" +#include "net/NetJob.h" +#include "settings/SettingsObject.h" +#include "tasks/Task.h" class InstanceCopyTask : public InstanceTask { Q_OBJECT public: - explicit InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime); + explicit InstanceCopyTask(InstancePtr origInstance, InstanceCopyPrefs prefs); protected: //! Entry point for tasks. @@ -22,7 +23,12 @@ protected: void copyFinished(); void copyAborted(); -private: /* data */ +private: + // Helper functions to avoid repeating code + static void appendToFilter(QString &filter, const QString &append); + void resetFromMatcher(const QString ®exp); + + /* data */ InstancePtr m_origInstance; QFuture m_copyFuture; QFutureWatcher m_copyFutureWatcher; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 97152a485..d51f799c6 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1616,7 +1616,17 @@ void MainWindow::on_actionCopyInstance_triggered() if (!copyInstDlg.exec()) return; - auto copyTask = new InstanceCopyTask(m_selectedInstance, copyInstDlg.shouldCopySaves(), copyInstDlg.shouldKeepPlaytime()); + auto copyTask = new InstanceCopyTask( + m_selectedInstance, + InstanceCopyPrefs { + copyInstDlg.shouldCopySaves(), + copyInstDlg.shouldKeepPlaytime(), + copyInstDlg.shouldCopyGameOptions(), + copyInstDlg.shouldCopyResourcePacks(), + copyInstDlg.shouldCopyShaderPacks(), + copyInstDlg.shouldCopyServers(), + copyInstDlg.shouldCopyMods() + }); copyTask->setName(copyInstDlg.instName()); copyTask->setGroup(copyInstDlg.instGroup()); copyTask->setIcon(copyInstDlg.iconKey()); diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index 9ec341bc8..d19888edb 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -80,6 +80,11 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent) ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); ui->copySavesCheckbox->setChecked(m_copySaves); ui->keepPlaytimeCheckbox->setChecked(m_keepPlaytime); + ui->copyGameOptionsCheckbox->setChecked(m_copyGameOptions); + ui->copyResPacksCheckbox->setChecked(m_copyResourcePacks); + ui->copyShaderPacksCheckbox->setChecked(m_copyShaderPacks); + ui->copyServersCheckbox->setChecked(m_copyServers); + ui->copyModsCheckbox->setChecked(m_copyMods); } CopyInstanceDialog::~CopyInstanceDialog() @@ -168,3 +173,88 @@ void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state) m_keepPlaytime = true; } } + +bool CopyInstanceDialog::shouldCopyGameOptions() const +{ + return m_copyGameOptions; +} + +void CopyInstanceDialog::on_copyGameOptionsCheckbox_stateChanged(int state) +{ + if(state == Qt::Unchecked) + { + m_copyGameOptions = false; + } + else if(state == Qt::Checked) + { + m_copyGameOptions = true; + } +} + +bool CopyInstanceDialog::shouldCopyResourcePacks() const +{ + return m_copyResourcePacks; +} + +void CopyInstanceDialog::on_copyResPacksCheckbox_stateChanged(int state) +{ + if(state == Qt::Unchecked) + { + m_copyResourcePacks = false; + } + else if(state == Qt::Checked) + { + m_copyResourcePacks = true; + } +} + +bool CopyInstanceDialog::shouldCopyShaderPacks() const +{ + return m_copyShaderPacks; +} + +void CopyInstanceDialog::on_copyShaderPacksCheckbox_stateChanged(int state) +{ + if(state == Qt::Unchecked) + { + m_copyShaderPacks = false; + } + else if(state == Qt::Checked) + { + m_copyShaderPacks = true; + } +} + +bool CopyInstanceDialog::shouldCopyServers() const +{ + return m_copyServers; +} + +void CopyInstanceDialog::on_copyServersCheckbox_stateChanged(int state) +{ + if(state == Qt::Unchecked) + { + m_copyServers = false; + } + else if(state == Qt::Checked) + { + m_copyServers = true; + } +} + +bool CopyInstanceDialog::shouldCopyMods() const +{ + return m_copyMods; +} + +void CopyInstanceDialog::on_copyModsCheckbox_stateChanged(int state) +{ + if(state == Qt::Unchecked) + { + m_copyMods = false; + } + else if(state == Qt::Checked) + { + m_copyMods = true; + } +} diff --git a/launcher/ui/dialogs/CopyInstanceDialog.h b/launcher/ui/dialogs/CopyInstanceDialog.h index bf3cd920b..e4c70494f 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.h +++ b/launcher/ui/dialogs/CopyInstanceDialog.h @@ -41,6 +41,11 @@ public: QString iconKey() const; bool shouldCopySaves() const; bool shouldKeepPlaytime() const; + bool shouldCopyGameOptions() const; + bool shouldCopyResourcePacks() const; + bool shouldCopyShaderPacks() const; + bool shouldCopyServers() const; + bool shouldCopyMods() const; private slots: @@ -48,6 +53,11 @@ slots: void on_instNameTextBox_textChanged(const QString &arg1); void on_copySavesCheckbox_stateChanged(int state); void on_keepPlaytimeCheckbox_stateChanged(int state); + void on_copyGameOptionsCheckbox_stateChanged(int state); + void on_copyResPacksCheckbox_stateChanged(int state); + void on_copyShaderPacksCheckbox_stateChanged(int state); + void on_copyServersCheckbox_stateChanged(int state); + void on_copyModsCheckbox_stateChanged(int state); private: Ui::CopyInstanceDialog *ui; @@ -55,4 +65,9 @@ private: InstancePtr m_original; bool m_copySaves = true; bool m_keepPlaytime = true; + bool m_copyGameOptions = true; + bool m_copyResourcePacks = true; + bool m_copyShaderPacks = true; + bool m_copyServers = true; + bool m_copyMods = true; }; diff --git a/launcher/ui/dialogs/CopyInstanceDialog.ui b/launcher/ui/dialogs/CopyInstanceDialog.ui index f4b191e27..e89439e6b 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.ui +++ b/launcher/ui/dialogs/CopyInstanceDialog.ui @@ -9,8 +9,8 @@ 0 0 - 345 - 323 + 265 + 425 @@ -33,7 +33,7 @@ - 40 + 60 20 @@ -123,6 +123,50 @@ + + + + Copy the in-game options like FOV, max framerate, etc. + + + Copy game options + + + + + + + true + + + Copy resource packs + + + + + + + Copy shader packs + + + + + + + Copy servers + + + + + + + Disabling this will still keep the mod loader (ex: Fabric, Quilt, etc.) but erase the mods folder and their configs. + + + Copy mods + + + @@ -153,8 +197,8 @@ accept() - 248 - 254 + 254 + 316 157 @@ -169,8 +213,8 @@ reject() - 316 - 260 + 322 + 316 286 From 15593b5c0912b4fe5ad77d6a27e336e9b68ed861 Mon Sep 17 00:00:00 2001 From: Marcelo Hernandez Date: Sat, 22 Oct 2022 23:04:36 -0400 Subject: [PATCH 03/59] Add "Select all" checkbox + ui revamp + code cleanup Signed-off-by: Marcelo Hernandez --- launcher/CMakeLists.txt | 1 + launcher/InstanceCopyPrefs.cpp | 15 +++ launcher/InstanceCopyPrefs.h | 3 + launcher/InstanceCopyTask.cpp | 4 +- launcher/InstanceCopyTask.h | 2 +- launcher/ui/MainWindow.cpp | 12 +- launcher/ui/dialogs/CopyInstanceDialog.cpp | 102 ++++++++-------- launcher/ui/dialogs/CopyInstanceDialog.h | 22 ++-- launcher/ui/dialogs/CopyInstanceDialog.ui | 136 +++++++++++---------- 9 files changed, 153 insertions(+), 144 deletions(-) create mode 100644 launcher/InstanceCopyPrefs.cpp diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 77440cca9..7dc060fb4 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -32,6 +32,7 @@ set(CORE_SOURCES InstanceCreationTask.h InstanceCreationTask.cpp InstanceCopyPrefs.h + InstanceCopyPrefs.cpp InstanceCopyTask.h InstanceCopyTask.cpp InstanceImportTask.h diff --git a/launcher/InstanceCopyPrefs.cpp b/launcher/InstanceCopyPrefs.cpp new file mode 100644 index 000000000..56b43a03e --- /dev/null +++ b/launcher/InstanceCopyPrefs.cpp @@ -0,0 +1,15 @@ +// +// Created by marcelohdez on 10/22/22. +// + +#include "InstanceCopyPrefs.h" + +InstanceCopyPrefs::InstanceCopyPrefs(bool setAll) + : copySaves(setAll), + keepPlaytime(setAll), + copyGameOptions(setAll), + copyResourcePacks(setAll), + copyShaderPacks(setAll), + copyServers(setAll), + copyMods(setAll) +{} diff --git a/launcher/InstanceCopyPrefs.h b/launcher/InstanceCopyPrefs.h index ac2feab8a..d360a8a73 100644 --- a/launcher/InstanceCopyPrefs.h +++ b/launcher/InstanceCopyPrefs.h @@ -6,6 +6,9 @@ #define LAUNCHER_INSTANCECOPYPREFS_H struct InstanceCopyPrefs { + explicit InstanceCopyPrefs(bool setAll); + ~InstanceCopyPrefs() = default; + bool copySaves; bool keepPlaytime; bool copyGameOptions; diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index 360f6cfad..e0f682244 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -5,7 +5,7 @@ #include "pathmatcher/RegexpMatcher.h" #include -InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, InstanceCopyPrefs prefs) +InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs) { m_origInstance = origInstance; m_keepPlaytime = prefs.keepPlaytime; @@ -51,7 +51,7 @@ InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, InstanceCopyPrefs p } } -void InstanceCopyTask::appendToFilter(QString& filter, const QString &append) +void InstanceCopyTask::appendToFilter(QString& filter, const QString& append) { if (!filter.isEmpty()) filter.append('|'); // OR regex diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h index d66bec55b..4abbf6e69 100644 --- a/launcher/InstanceCopyTask.h +++ b/launcher/InstanceCopyTask.h @@ -15,7 +15,7 @@ class InstanceCopyTask : public InstanceTask { Q_OBJECT public: - explicit InstanceCopyTask(InstancePtr origInstance, InstanceCopyPrefs prefs); + explicit InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs); protected: //! Entry point for tasks. diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index d51f799c6..08005b86c 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1616,17 +1616,7 @@ void MainWindow::on_actionCopyInstance_triggered() if (!copyInstDlg.exec()) return; - auto copyTask = new InstanceCopyTask( - m_selectedInstance, - InstanceCopyPrefs { - copyInstDlg.shouldCopySaves(), - copyInstDlg.shouldKeepPlaytime(), - copyInstDlg.shouldCopyGameOptions(), - copyInstDlg.shouldCopyResourcePacks(), - copyInstDlg.shouldCopyShaderPacks(), - copyInstDlg.shouldCopyServers(), - copyInstDlg.shouldCopyMods() - }); + auto copyTask = new InstanceCopyTask(m_selectedInstance, copyInstDlg.getChosenOptions()); copyTask->setName(copyInstDlg.instName()); copyTask->setGroup(copyInstDlg.instGroup()); copyTask->setIcon(copyInstDlg.iconKey()); diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index d19888edb..0a23cd34f 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -78,13 +78,13 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent) } ui->groupBox->setCurrentIndex(index); ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); - ui->copySavesCheckbox->setChecked(m_copySaves); - ui->keepPlaytimeCheckbox->setChecked(m_keepPlaytime); - ui->copyGameOptionsCheckbox->setChecked(m_copyGameOptions); - ui->copyResPacksCheckbox->setChecked(m_copyResourcePacks); - ui->copyShaderPacksCheckbox->setChecked(m_copyShaderPacks); - ui->copyServersCheckbox->setChecked(m_copyServers); - ui->copyModsCheckbox->setChecked(m_copyMods); + ui->copySavesCheckbox->setChecked(m_selectedOptions.copySaves); + ui->keepPlaytimeCheckbox->setChecked(m_selectedOptions.keepPlaytime); + ui->copyGameOptionsCheckbox->setChecked(m_selectedOptions.copyGameOptions); + ui->copyResPacksCheckbox->setChecked(m_selectedOptions.copyResourcePacks); + ui->copyShaderPacksCheckbox->setChecked(m_selectedOptions.copyShaderPacks); + ui->copyServersCheckbox->setChecked(m_selectedOptions.copyServers); + ui->copyModsCheckbox->setChecked(m_selectedOptions.copyMods); } CopyInstanceDialog::~CopyInstanceDialog() @@ -139,122 +139,118 @@ void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1) updateDialogState(); } -bool CopyInstanceDialog::shouldCopySaves() const +const InstanceCopyPrefs& CopyInstanceDialog::getChosenOptions() const { - return m_copySaves; + return m_selectedOptions; +} + +void CopyInstanceDialog::on_selectAllCheckbox_stateChanged(int state) +{ + bool checked; + if(state == Qt::Unchecked) + { + checked = false; + } + else if(state == Qt::Checked) + { + checked = true; + } + + checkAllCheckboxes(checked); +} + +void CopyInstanceDialog::checkAllCheckboxes(bool b) +{ + ui->keepPlaytimeCheckbox->setChecked(b); + ui->copySavesCheckbox->setChecked(b); + ui->copyGameOptionsCheckbox->setChecked(b); + ui->copyResPacksCheckbox->setChecked(b); + ui->copyShaderPacksCheckbox->setChecked(b); + ui->copyServersCheckbox->setChecked(b); + ui->copyModsCheckbox->setChecked(b); } void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state) { if(state == Qt::Unchecked) { - m_copySaves = false; + m_selectedOptions.copySaves = false; } else if(state == Qt::Checked) { - m_copySaves = true; + m_selectedOptions.copySaves = true; } } -bool CopyInstanceDialog::shouldKeepPlaytime() const -{ - return m_keepPlaytime; -} - void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state) { if(state == Qt::Unchecked) { - m_keepPlaytime = false; + m_selectedOptions.keepPlaytime = false; } else if(state == Qt::Checked) { - m_keepPlaytime = true; + m_selectedOptions.keepPlaytime = true; } } -bool CopyInstanceDialog::shouldCopyGameOptions() const -{ - return m_copyGameOptions; -} - void CopyInstanceDialog::on_copyGameOptionsCheckbox_stateChanged(int state) { if(state == Qt::Unchecked) { - m_copyGameOptions = false; + m_selectedOptions.copyGameOptions = false; } else if(state == Qt::Checked) { - m_copyGameOptions = true; + m_selectedOptions.copyGameOptions = true; } } -bool CopyInstanceDialog::shouldCopyResourcePacks() const -{ - return m_copyResourcePacks; -} - void CopyInstanceDialog::on_copyResPacksCheckbox_stateChanged(int state) { if(state == Qt::Unchecked) { - m_copyResourcePacks = false; + m_selectedOptions.copyResourcePacks = false; } else if(state == Qt::Checked) { - m_copyResourcePacks = true; + m_selectedOptions.copyResourcePacks = true; } } -bool CopyInstanceDialog::shouldCopyShaderPacks() const -{ - return m_copyShaderPacks; -} - void CopyInstanceDialog::on_copyShaderPacksCheckbox_stateChanged(int state) { if(state == Qt::Unchecked) { - m_copyShaderPacks = false; + m_selectedOptions.copyShaderPacks = false; } else if(state == Qt::Checked) { - m_copyShaderPacks = true; + m_selectedOptions.copyShaderPacks = true; } } -bool CopyInstanceDialog::shouldCopyServers() const -{ - return m_copyServers; -} - void CopyInstanceDialog::on_copyServersCheckbox_stateChanged(int state) { if(state == Qt::Unchecked) { - m_copyServers = false; + m_selectedOptions.copyServers = false; } else if(state == Qt::Checked) { - m_copyServers = true; + m_selectedOptions.copyServers = true; } } -bool CopyInstanceDialog::shouldCopyMods() const -{ - return m_copyMods; -} - void CopyInstanceDialog::on_copyModsCheckbox_stateChanged(int state) { if(state == Qt::Unchecked) { - m_copyMods = false; + m_selectedOptions.copyMods = false; } else if(state == Qt::Checked) { - m_copyMods = true; + m_selectedOptions.copyMods = true; } } diff --git a/launcher/ui/dialogs/CopyInstanceDialog.h b/launcher/ui/dialogs/CopyInstanceDialog.h index e4c70494f..e57de0a1e 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.h +++ b/launcher/ui/dialogs/CopyInstanceDialog.h @@ -17,7 +17,7 @@ #include #include "BaseVersion.h" -#include +#include "InstanceCopyPrefs.h" class BaseInstance; @@ -39,18 +39,16 @@ public: QString instName() const; QString instGroup() const; QString iconKey() const; - bool shouldCopySaves() const; - bool shouldKeepPlaytime() const; - bool shouldCopyGameOptions() const; - bool shouldCopyResourcePacks() const; - bool shouldCopyShaderPacks() const; - bool shouldCopyServers() const; - bool shouldCopyMods() const; + const InstanceCopyPrefs& getChosenOptions() const; private slots: void on_iconButton_clicked(); void on_instNameTextBox_textChanged(const QString &arg1); + + // Checkbox options: + void checkAllCheckboxes(bool b); + void on_selectAllCheckbox_stateChanged(int state); void on_copySavesCheckbox_stateChanged(int state); void on_keepPlaytimeCheckbox_stateChanged(int state); void on_copyGameOptionsCheckbox_stateChanged(int state); @@ -63,11 +61,5 @@ private: Ui::CopyInstanceDialog *ui; QString InstIconKey; InstancePtr m_original; - bool m_copySaves = true; - bool m_keepPlaytime = true; - bool m_copyGameOptions = true; - bool m_copyResourcePacks = true; - bool m_copyShaderPacks = true; - bool m_copyServers = true; - bool m_copyMods = true; + InstanceCopyPrefs m_selectedOptions = InstanceCopyPrefs(true); // Default to all options as true }; diff --git a/launcher/ui/dialogs/CopyInstanceDialog.ui b/launcher/ui/dialogs/CopyInstanceDialog.ui index e89439e6b..822ed7977 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.ui +++ b/launcher/ui/dialogs/CopyInstanceDialog.ui @@ -9,8 +9,8 @@ 0 0 - 265 - 425 + 341 + 385 @@ -60,7 +60,7 @@ - 40 + 60 20 @@ -83,7 +83,10 @@ - + + + 6 + @@ -110,62 +113,73 @@ - - - Copy saves - - - - - - - Keep play time - - - - - - - Copy the in-game options like FOV, max framerate, etc. - - - Copy game options - - - - - - - true - - - Copy resource packs - - - - - - - Copy shader packs - - - - - - - Copy servers - - - - - - - Disabling this will still keep the mod loader (ex: Fabric, Quilt, etc.) but erase the mods folder and their configs. - - - Copy mods - - + + + + + Copy saves + + + + + + + true + + + Copy resource packs + + + + + + + Disabling this will still keep the mod loader (ex: Fabric, Quilt, etc.) but erase the mods folder and their configs. + + + Copy mods + + + + + + + Copy the in-game options like FOV, max framerate, etc. + + + Copy game options + + + + + + + Copy servers + + + + + + + Keep play time + + + + + + + Copy shader packs + + + + + + + Select all + + + + @@ -183,8 +197,6 @@ iconButton instNameTextBox groupBox - copySavesCheckbox - keepPlaytimeCheckbox From 4caf06bc99dfe34f10fae943374c98b88ad8814d Mon Sep 17 00:00:00 2001 From: Marcelo Hernandez Date: Sun, 23 Oct 2022 00:25:38 -0400 Subject: [PATCH 04/59] Check "Select all" checkbox if all options are already selected + code cleanup Signed-off-by: Marcelo Hernandez --- launcher/InstanceCopyPrefs.cpp | 19 ++-- launcher/InstanceCopyPrefs.h | 17 ++- launcher/ui/dialogs/CopyInstanceDialog.cpp | 124 +++++++-------------- launcher/ui/dialogs/CopyInstanceDialog.h | 6 +- 4 files changed, 65 insertions(+), 101 deletions(-) diff --git a/launcher/InstanceCopyPrefs.cpp b/launcher/InstanceCopyPrefs.cpp index 56b43a03e..fad55d1e5 100644 --- a/launcher/InstanceCopyPrefs.cpp +++ b/launcher/InstanceCopyPrefs.cpp @@ -4,12 +4,13 @@ #include "InstanceCopyPrefs.h" -InstanceCopyPrefs::InstanceCopyPrefs(bool setAll) - : copySaves(setAll), - keepPlaytime(setAll), - copyGameOptions(setAll), - copyResourcePacks(setAll), - copyShaderPacks(setAll), - copyServers(setAll), - copyMods(setAll) -{} +bool InstanceCopyPrefs::allTrue() const +{ + return copySaves && + keepPlaytime && + copyGameOptions && + copyResourcePacks && + copyShaderPacks && + copyServers && + copyMods; +} diff --git a/launcher/InstanceCopyPrefs.h b/launcher/InstanceCopyPrefs.h index d360a8a73..c5c1a7ae2 100644 --- a/launcher/InstanceCopyPrefs.h +++ b/launcher/InstanceCopyPrefs.h @@ -6,16 +6,15 @@ #define LAUNCHER_INSTANCECOPYPREFS_H struct InstanceCopyPrefs { - explicit InstanceCopyPrefs(bool setAll); - ~InstanceCopyPrefs() = default; + bool copySaves = true; + bool keepPlaytime = true; + bool copyGameOptions = true; + bool copyResourcePacks = true; + bool copyShaderPacks = true; + bool copyServers = true; + bool copyMods = true; - bool copySaves; - bool keepPlaytime; - bool copyGameOptions; - bool copyResourcePacks; - bool copyShaderPacks; - bool copyServers; - bool copyMods; + bool allTrue() const; }; #endif // LAUNCHER_INSTANCECOPYPREFS_H diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index 0a23cd34f..44e70012d 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -122,6 +122,40 @@ QString CopyInstanceDialog::instGroup() const return ui->groupBox->currentText(); } +const InstanceCopyPrefs& CopyInstanceDialog::getChosenOptions() const +{ + return m_selectedOptions; +} + +void CopyInstanceDialog::checkAllCheckboxes(const bool& b) +{ + ui->keepPlaytimeCheckbox->setChecked(b); + ui->copySavesCheckbox->setChecked(b); + ui->copyGameOptionsCheckbox->setChecked(b); + ui->copyResPacksCheckbox->setChecked(b); + ui->copyShaderPacksCheckbox->setChecked(b); + ui->copyServersCheckbox->setChecked(b); + ui->copyModsCheckbox->setChecked(b); +} + +// Sets b to true if state is a checked checkbox +void CopyInstanceDialog::checkBool(bool& b, const int& state) +{ + if(state == Qt::Unchecked) + { + b = false; + } + else if(state == Qt::Checked) + { + b = true; + } + + // Have "Select all" checkbox checked if all options are already checked: + ui->selectAllCheckbox->blockSignals(true); + ui->selectAllCheckbox->setChecked(m_selectedOptions.allTrue()); + ui->selectAllCheckbox->blockSignals(false); +} + void CopyInstanceDialog::on_iconButton_clicked() { IconPickerDialog dlg(this); @@ -134,123 +168,51 @@ void CopyInstanceDialog::on_iconButton_clicked() } } + void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1) { updateDialogState(); } -const InstanceCopyPrefs& CopyInstanceDialog::getChosenOptions() const -{ - return m_selectedOptions; -} - void CopyInstanceDialog::on_selectAllCheckbox_stateChanged(int state) { bool checked; - if(state == Qt::Unchecked) - { - checked = false; - } - else if(state == Qt::Checked) - { - checked = true; - } - + checkBool(checked, state); checkAllCheckboxes(checked); } -void CopyInstanceDialog::checkAllCheckboxes(bool b) -{ - ui->keepPlaytimeCheckbox->setChecked(b); - ui->copySavesCheckbox->setChecked(b); - ui->copyGameOptionsCheckbox->setChecked(b); - ui->copyResPacksCheckbox->setChecked(b); - ui->copyShaderPacksCheckbox->setChecked(b); - ui->copyServersCheckbox->setChecked(b); - ui->copyModsCheckbox->setChecked(b); -} - void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state) { - if(state == Qt::Unchecked) - { - m_selectedOptions.copySaves = false; - } - else if(state == Qt::Checked) - { - m_selectedOptions.copySaves = true; - } + checkBool(m_selectedOptions.copySaves, state); } void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state) { - if(state == Qt::Unchecked) - { - m_selectedOptions.keepPlaytime = false; - } - else if(state == Qt::Checked) - { - m_selectedOptions.keepPlaytime = true; - } + checkBool(m_selectedOptions.keepPlaytime, state); } void CopyInstanceDialog::on_copyGameOptionsCheckbox_stateChanged(int state) { - if(state == Qt::Unchecked) - { - m_selectedOptions.copyGameOptions = false; - } - else if(state == Qt::Checked) - { - m_selectedOptions.copyGameOptions = true; - } + checkBool(m_selectedOptions.copyGameOptions, state); } void CopyInstanceDialog::on_copyResPacksCheckbox_stateChanged(int state) { - if(state == Qt::Unchecked) - { - m_selectedOptions.copyResourcePacks = false; - } - else if(state == Qt::Checked) - { - m_selectedOptions.copyResourcePacks = true; - } + checkBool(m_selectedOptions.copyResourcePacks, state); } void CopyInstanceDialog::on_copyShaderPacksCheckbox_stateChanged(int state) { - if(state == Qt::Unchecked) - { - m_selectedOptions.copyShaderPacks = false; - } - else if(state == Qt::Checked) - { - m_selectedOptions.copyShaderPacks = true; - } + checkBool(m_selectedOptions.copyShaderPacks, state); } void CopyInstanceDialog::on_copyServersCheckbox_stateChanged(int state) { - if(state == Qt::Unchecked) - { - m_selectedOptions.copyServers = false; - } - else if(state == Qt::Checked) - { - m_selectedOptions.copyServers = true; - } + checkBool(m_selectedOptions.copyServers, state); } void CopyInstanceDialog::on_copyModsCheckbox_stateChanged(int state) { - if(state == Qt::Unchecked) - { - m_selectedOptions.copyMods = false; - } - else if(state == Qt::Checked) - { - m_selectedOptions.copyMods = true; - } + checkBool(m_selectedOptions.copyMods, state); } diff --git a/launcher/ui/dialogs/CopyInstanceDialog.h b/launcher/ui/dialogs/CopyInstanceDialog.h index e57de0a1e..4171c440f 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.h +++ b/launcher/ui/dialogs/CopyInstanceDialog.h @@ -47,7 +47,9 @@ slots: void on_instNameTextBox_textChanged(const QString &arg1); // Checkbox options: - void checkAllCheckboxes(bool b); + void checkAllCheckboxes(const bool& b); + void checkBool(bool& b, const int& state); + void on_selectAllCheckbox_stateChanged(int state); void on_copySavesCheckbox_stateChanged(int state); void on_keepPlaytimeCheckbox_stateChanged(int state); @@ -61,5 +63,5 @@ private: Ui::CopyInstanceDialog *ui; QString InstIconKey; InstancePtr m_original; - InstanceCopyPrefs m_selectedOptions = InstanceCopyPrefs(true); // Default to all options as true + InstanceCopyPrefs m_selectedOptions; }; From a89df42561cc3089c4878c0c44353fcd1359bf53 Mon Sep 17 00:00:00 2001 From: Marcelo Hernandez <76508651+marcelohdez@users.noreply.github.com> Date: Mon, 24 Oct 2022 19:27:21 -0400 Subject: [PATCH 05/59] Simplify bool check in CopyInstanceDialog.cpp Signed-off-by: Marcelo Hernandez --- launcher/ui/dialogs/CopyInstanceDialog.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index 44e70012d..1b8e2aa0b 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -141,14 +141,7 @@ void CopyInstanceDialog::checkAllCheckboxes(const bool& b) // Sets b to true if state is a checked checkbox void CopyInstanceDialog::checkBool(bool& b, const int& state) { - if(state == Qt::Unchecked) - { - b = false; - } - else if(state == Qt::Checked) - { - b = true; - } + b = (state == Qt::Checked); // Have "Select all" checkbox checked if all options are already checked: ui->selectAllCheckbox->blockSignals(true); From 385c452ddffa2f40b21d7decede9f255e2b24d45 Mon Sep 17 00:00:00 2001 From: Marcelo Hernandez Date: Mon, 24 Oct 2022 20:49:40 -0400 Subject: [PATCH 06/59] remove checkBool function, add updateSelectAllCheckbox function Signed-off-by: Marcelo Hernandez --- launcher/ui/dialogs/CopyInstanceDialog.cpp | 30 ++++++++++++---------- launcher/ui/dialogs/CopyInstanceDialog.h | 9 +++---- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index 1b8e2aa0b..8445f0a91 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -138,12 +138,9 @@ void CopyInstanceDialog::checkAllCheckboxes(const bool& b) ui->copyModsCheckbox->setChecked(b); } -// Sets b to true if state is a checked checkbox -void CopyInstanceDialog::checkBool(bool& b, const int& state) +// Check the "Select all" checkbox checked if all options are already checked: +void CopyInstanceDialog::updateSelectAllCheckbox() { - b = (state == Qt::Checked); - - // Have "Select all" checkbox checked if all options are already checked: ui->selectAllCheckbox->blockSignals(true); ui->selectAllCheckbox->setChecked(m_selectedOptions.allTrue()); ui->selectAllCheckbox->blockSignals(false); @@ -170,42 +167,49 @@ void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1) void CopyInstanceDialog::on_selectAllCheckbox_stateChanged(int state) { bool checked; - checkBool(checked, state); + checked = (state == Qt::Checked); checkAllCheckboxes(checked); } void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state) { - checkBool(m_selectedOptions.copySaves, state); + m_selectedOptions.copySaves = (state == Qt::Checked); + updateSelectAllCheckbox(); } void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state) { - checkBool(m_selectedOptions.keepPlaytime, state); + m_selectedOptions.keepPlaytime = (state == Qt::Checked); + updateSelectAllCheckbox(); } void CopyInstanceDialog::on_copyGameOptionsCheckbox_stateChanged(int state) { - checkBool(m_selectedOptions.copyGameOptions, state); + m_selectedOptions.copyGameOptions = (state == Qt::Checked); + updateSelectAllCheckbox(); } void CopyInstanceDialog::on_copyResPacksCheckbox_stateChanged(int state) { - checkBool(m_selectedOptions.copyResourcePacks, state); + m_selectedOptions.copyResourcePacks = (state == Qt::Checked); + updateSelectAllCheckbox(); } void CopyInstanceDialog::on_copyShaderPacksCheckbox_stateChanged(int state) { - checkBool(m_selectedOptions.copyShaderPacks, state); + m_selectedOptions.copyShaderPacks = (state == Qt::Checked); + updateSelectAllCheckbox(); } void CopyInstanceDialog::on_copyServersCheckbox_stateChanged(int state) { - checkBool(m_selectedOptions.copyServers, state); + m_selectedOptions.copyServers = (state == Qt::Checked); + updateSelectAllCheckbox(); } void CopyInstanceDialog::on_copyModsCheckbox_stateChanged(int state) { - checkBool(m_selectedOptions.copyMods, state); + m_selectedOptions.copyMods = (state == Qt::Checked); + updateSelectAllCheckbox(); } diff --git a/launcher/ui/dialogs/CopyInstanceDialog.h b/launcher/ui/dialogs/CopyInstanceDialog.h index 4171c440f..94015334e 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.h +++ b/launcher/ui/dialogs/CopyInstanceDialog.h @@ -45,11 +45,7 @@ private slots: void on_iconButton_clicked(); void on_instNameTextBox_textChanged(const QString &arg1); - - // Checkbox options: - void checkAllCheckboxes(const bool& b); - void checkBool(bool& b, const int& state); - + // Checkboxes void on_selectAllCheckbox_stateChanged(int state); void on_copySavesCheckbox_stateChanged(int state); void on_keepPlaytimeCheckbox_stateChanged(int state); @@ -60,6 +56,9 @@ slots: void on_copyModsCheckbox_stateChanged(int state); private: + void checkAllCheckboxes(const bool& b); + void updateSelectAllCheckbox(); + /* data */ Ui::CopyInstanceDialog *ui; QString InstIconKey; InstancePtr m_original; From 63b6c6685ce53e3fac1902e0ee7a6998c5d341d0 Mon Sep 17 00:00:00 2001 From: Marcelo Hernandez Date: Wed, 26 Oct 2022 00:20:36 -0400 Subject: [PATCH 07/59] Abstract away InstanceCopyPrefs' internals through new getSelectedFiltersAsRegex() function + fix typo in comment + remove unused import + add [[nodiscard]] to methods Signed-off-by: Marcelo Hernandez --- launcher/InstanceCopyPrefs.cpp | 33 ++++++++++++ launcher/InstanceCopyPrefs.h | 5 +- launcher/InstanceCopyTask.cpp | 60 +++------------------- launcher/InstanceCopyTask.h | 4 -- launcher/ui/dialogs/CopyInstanceDialog.cpp | 3 +- 5 files changed, 45 insertions(+), 60 deletions(-) diff --git a/launcher/InstanceCopyPrefs.cpp b/launcher/InstanceCopyPrefs.cpp index fad55d1e5..6432a5351 100644 --- a/launcher/InstanceCopyPrefs.cpp +++ b/launcher/InstanceCopyPrefs.cpp @@ -14,3 +14,36 @@ bool InstanceCopyPrefs::allTrue() const copyServers && copyMods; } + +// Returns a single RegEx string of the selected folders/files to filter out (ex: ".minecraft/saves|.minecraft/server.dat") +QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const +{ + QStringList filters; + + if(!copySaves) + filters << "saves"; + + if(!copyGameOptions) + filters << "options.txt"; + + if(!copyResourcePacks) + filters << "resourcepacks" << "texturepacks"; + + if(!copyShaderPacks) + filters << "shaderpacks"; + + if(!copyServers) + filters << "servers.dat" << "servers.dat_old" << "server-resource-packs"; + + if(!copyMods) + filters << "coremods" << "mods" << "config"; + + // If we have any filters to add, join them as a single regex string to return: + if (!filters.isEmpty()) { + const QString MC_ROOT = "[.]?minecraft/"; + // Ensure first filter starts with root, then join other filters with OR regex before root (ex: ".minecraft/saves|.minecraft/mods"): + return MC_ROOT + filters.join("|" + MC_ROOT); + } + + return {}; +} diff --git a/launcher/InstanceCopyPrefs.h b/launcher/InstanceCopyPrefs.h index c5c1a7ae2..432d67c40 100644 --- a/launcher/InstanceCopyPrefs.h +++ b/launcher/InstanceCopyPrefs.h @@ -5,6 +5,8 @@ #ifndef LAUNCHER_INSTANCECOPYPREFS_H #define LAUNCHER_INSTANCECOPYPREFS_H +#include + struct InstanceCopyPrefs { bool copySaves = true; bool keepPlaytime = true; @@ -14,7 +16,8 @@ struct InstanceCopyPrefs { bool copyServers = true; bool copyMods = true; - bool allTrue() const; + [[nodiscard]] bool allTrue() const; + [[nodiscard]] QString getSelectedFiltersAsRegex() const; }; #endif // LAUNCHER_INSTANCECOPYPREFS_H diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index e0f682244..7fbf86363 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -9,62 +9,16 @@ InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyP { m_origInstance = origInstance; m_keepPlaytime = prefs.keepPlaytime; - QString filter; - if(!prefs.copySaves) + QString filters = prefs.getSelectedFiltersAsRegex(); + if (!filters.isEmpty()) { - appendToFilter(filter, "saves"); + // Set regex filter: + // FIXME: get this from the original instance type... + auto matcherReal = new RegexpMatcher(filters); + matcherReal->caseSensitive(false); + m_matcher.reset(matcherReal); } - - if(!prefs.copyGameOptions) { - appendToFilter(filter, "options.txt"); - } - - if(!prefs.copyResourcePacks) - { - appendToFilter(filter, "resourcepacks"); - appendToFilter(filter, "texturepacks"); - } - - if(!prefs.copyShaderPacks) - { - appendToFilter(filter, "shaderpacks"); - } - - if(!prefs.copyServers) - { - appendToFilter(filter, "servers.dat"); - appendToFilter(filter, "servers.dat_old"); - appendToFilter(filter, "server-resource-packs"); - } - - if(!prefs.copyMods) - { - appendToFilter(filter, "coremods"); - appendToFilter(filter, "mods"); - appendToFilter(filter, "config"); - } - - if (!filter.isEmpty()) - { - resetFromMatcher(filter); - } -} - -void InstanceCopyTask::appendToFilter(QString& filter, const QString& append) -{ - if (!filter.isEmpty()) - filter.append('|'); // OR regex - - filter.append("[.]?minecraft/" + append); -} - -void InstanceCopyTask::resetFromMatcher(const QString& regexp) -{ - // FIXME: get this from the original instance type... - auto matcherReal = new RegexpMatcher(regexp); - matcherReal->caseSensitive(false); - m_matcher.reset(matcherReal); } void InstanceCopyTask::executeTask() diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h index 4abbf6e69..1f29b8545 100644 --- a/launcher/InstanceCopyTask.h +++ b/launcher/InstanceCopyTask.h @@ -24,10 +24,6 @@ protected: void copyAborted(); private: - // Helper functions to avoid repeating code - static void appendToFilter(QString &filter, const QString &append); - void resetFromMatcher(const QString ®exp); - /* data */ InstancePtr m_origInstance; QFuture m_copyFuture; diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index 8445f0a91..e658f26d5 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -44,7 +44,6 @@ #include "BaseVersion.h" #include "icons/IconList.h" -#include "tasks/Task.h" #include "BaseInstance.h" #include "InstanceList.h" @@ -138,7 +137,7 @@ void CopyInstanceDialog::checkAllCheckboxes(const bool& b) ui->copyModsCheckbox->setChecked(b); } -// Check the "Select all" checkbox checked if all options are already checked: +// Check the "Select all" checkbox if all options are already selected: void CopyInstanceDialog::updateSelectAllCheckbox() { ui->selectAllCheckbox->blockSignals(true); From c00f96c7ca49a624ea8e9c4774ea11e954bbdc4b Mon Sep 17 00:00:00 2001 From: Marcelo Hernandez Date: Sat, 29 Oct 2022 00:55:33 -0400 Subject: [PATCH 08/59] create getters and setters for InstanceCopyPrefs + use pragma once like other .h files in this directory Signed-off-by: Marcelo Hernandez --- launcher/InstanceCopyPrefs.cpp | 72 ++++++++++++++++++++++ launcher/InstanceCopyPrefs.h | 29 ++++++--- launcher/InstanceCopyTask.cpp | 2 +- launcher/ui/dialogs/CopyInstanceDialog.cpp | 28 ++++----- 4 files changed, 109 insertions(+), 22 deletions(-) diff --git a/launcher/InstanceCopyPrefs.cpp b/launcher/InstanceCopyPrefs.cpp index 6432a5351..ae30bb824 100644 --- a/launcher/InstanceCopyPrefs.cpp +++ b/launcher/InstanceCopyPrefs.cpp @@ -47,3 +47,75 @@ QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const return {}; } + +// ======= Getters ======= +bool InstanceCopyPrefs::isCopySavesEnabled() const +{ + return copySaves; +} + +bool InstanceCopyPrefs::isKeepPlaytimeEnabled() const +{ + return keepPlaytime; +} + +bool InstanceCopyPrefs::isCopyGameOptionsEnabled() const +{ + return copyGameOptions; +} + +bool InstanceCopyPrefs::isCopyResourcePacksEnabled() const +{ + return copyResourcePacks; +} + +bool InstanceCopyPrefs::isCopyShaderPacksEnabled() const +{ + return copyShaderPacks; +} + +bool InstanceCopyPrefs::isCopyServersEnabled() const +{ + return copyServers; +} + +bool InstanceCopyPrefs::isCopyModsEnabled() const +{ + return copyMods; +} + +// ======= Setters ======= +void InstanceCopyPrefs::enableCopySaves(bool b) +{ + copySaves = b; +} + +void InstanceCopyPrefs::enableKeepPlaytime(bool b) +{ + keepPlaytime = b; +} + +void InstanceCopyPrefs::enableCopyGameOptions(bool b) +{ + copyGameOptions = b; +} + +void InstanceCopyPrefs::enableCopyResourcePacks(bool b) +{ + copyResourcePacks = b; +} + +void InstanceCopyPrefs::enableCopyShaderPacks(bool b) +{ + copyShaderPacks = b; +} + +void InstanceCopyPrefs::enableCopyServers(bool b) +{ + copyServers = b; +} + +void InstanceCopyPrefs::enableCopyMods(bool b) +{ + copyMods = b; +} diff --git a/launcher/InstanceCopyPrefs.h b/launcher/InstanceCopyPrefs.h index 432d67c40..3855965de 100644 --- a/launcher/InstanceCopyPrefs.h +++ b/launcher/InstanceCopyPrefs.h @@ -2,12 +2,32 @@ // Created by marcelohdez on 10/22/22. // -#ifndef LAUNCHER_INSTANCECOPYPREFS_H -#define LAUNCHER_INSTANCECOPYPREFS_H +#pragma once #include struct InstanceCopyPrefs { + public: + [[nodiscard]] bool allTrue() const; + [[nodiscard]] QString getSelectedFiltersAsRegex() const; + // Getters + [[nodiscard]] bool isCopySavesEnabled() const; + [[nodiscard]] bool isKeepPlaytimeEnabled() const; + [[nodiscard]] bool isCopyGameOptionsEnabled() const; + [[nodiscard]] bool isCopyResourcePacksEnabled() const; + [[nodiscard]] bool isCopyShaderPacksEnabled() const; + [[nodiscard]] bool isCopyServersEnabled() const; + [[nodiscard]] bool isCopyModsEnabled() const; + // Setters + void enableCopySaves(bool b); + void enableKeepPlaytime(bool b); + void enableCopyGameOptions(bool b); + void enableCopyResourcePacks(bool b); + void enableCopyShaderPacks(bool b); + void enableCopyServers(bool b); + void enableCopyMods(bool b); + + protected: // data bool copySaves = true; bool keepPlaytime = true; bool copyGameOptions = true; @@ -15,9 +35,4 @@ struct InstanceCopyPrefs { bool copyShaderPacks = true; bool copyServers = true; bool copyMods = true; - - [[nodiscard]] bool allTrue() const; - [[nodiscard]] QString getSelectedFiltersAsRegex() const; }; - -#endif // LAUNCHER_INSTANCECOPYPREFS_H diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index 7fbf86363..a4ea947d2 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -8,7 +8,7 @@ InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs) { m_origInstance = origInstance; - m_keepPlaytime = prefs.keepPlaytime; + m_keepPlaytime = prefs.isKeepPlaytimeEnabled(); QString filters = prefs.getSelectedFiltersAsRegex(); if (!filters.isEmpty()) diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index e658f26d5..f76b509ea 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -77,13 +77,13 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent) } ui->groupBox->setCurrentIndex(index); ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); - ui->copySavesCheckbox->setChecked(m_selectedOptions.copySaves); - ui->keepPlaytimeCheckbox->setChecked(m_selectedOptions.keepPlaytime); - ui->copyGameOptionsCheckbox->setChecked(m_selectedOptions.copyGameOptions); - ui->copyResPacksCheckbox->setChecked(m_selectedOptions.copyResourcePacks); - ui->copyShaderPacksCheckbox->setChecked(m_selectedOptions.copyShaderPacks); - ui->copyServersCheckbox->setChecked(m_selectedOptions.copyServers); - ui->copyModsCheckbox->setChecked(m_selectedOptions.copyMods); + ui->copySavesCheckbox->setChecked(m_selectedOptions.isCopySavesEnabled()); + ui->keepPlaytimeCheckbox->setChecked(m_selectedOptions.isKeepPlaytimeEnabled()); + ui->copyGameOptionsCheckbox->setChecked(m_selectedOptions.isCopyGameOptionsEnabled()); + ui->copyResPacksCheckbox->setChecked(m_selectedOptions.isCopyResourcePacksEnabled()); + ui->copyShaderPacksCheckbox->setChecked(m_selectedOptions.isCopyShaderPacksEnabled()); + ui->copyServersCheckbox->setChecked(m_selectedOptions.isCopyServersEnabled()); + ui->copyModsCheckbox->setChecked(m_selectedOptions.isCopyModsEnabled()); } CopyInstanceDialog::~CopyInstanceDialog() @@ -172,43 +172,43 @@ void CopyInstanceDialog::on_selectAllCheckbox_stateChanged(int state) void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state) { - m_selectedOptions.copySaves = (state == Qt::Checked); + m_selectedOptions.enableCopySaves(state == Qt::Checked); updateSelectAllCheckbox(); } void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state) { - m_selectedOptions.keepPlaytime = (state == Qt::Checked); + m_selectedOptions.enableKeepPlaytime(state == Qt::Checked); updateSelectAllCheckbox(); } void CopyInstanceDialog::on_copyGameOptionsCheckbox_stateChanged(int state) { - m_selectedOptions.copyGameOptions = (state == Qt::Checked); + m_selectedOptions.enableCopyGameOptions(state == Qt::Checked); updateSelectAllCheckbox(); } void CopyInstanceDialog::on_copyResPacksCheckbox_stateChanged(int state) { - m_selectedOptions.copyResourcePacks = (state == Qt::Checked); + m_selectedOptions.enableCopyResourcePacks(state == Qt::Checked); updateSelectAllCheckbox(); } void CopyInstanceDialog::on_copyShaderPacksCheckbox_stateChanged(int state) { - m_selectedOptions.copyShaderPacks = (state == Qt::Checked); + m_selectedOptions.enableCopyShaderPacks(state == Qt::Checked); updateSelectAllCheckbox(); } void CopyInstanceDialog::on_copyServersCheckbox_stateChanged(int state) { - m_selectedOptions.copyServers = (state == Qt::Checked); + m_selectedOptions.enableCopyServers(state == Qt::Checked); updateSelectAllCheckbox(); } void CopyInstanceDialog::on_copyModsCheckbox_stateChanged(int state) { - m_selectedOptions.copyMods = (state == Qt::Checked); + m_selectedOptions.enableCopyMods(state == Qt::Checked); updateSelectAllCheckbox(); } From 5d1aac3c53904f7c843dc5cfdbdd33086eb4b6d6 Mon Sep 17 00:00:00 2001 From: Marcelo Hernandez Date: Sat, 29 Oct 2022 22:27:31 -0400 Subject: [PATCH 09/59] added option to not copy screenshots + moved select all checkbox to top row, centered. Signed-off-by: Marcelo Hernandez --- launcher/InstanceCopyPrefs.cpp | 16 +++- launcher/InstanceCopyPrefs.h | 3 + launcher/ui/dialogs/CopyInstanceDialog.cpp | 8 ++ launcher/ui/dialogs/CopyInstanceDialog.h | 1 + launcher/ui/dialogs/CopyInstanceDialog.ui | 85 ++++++++++++++-------- 5 files changed, 81 insertions(+), 32 deletions(-) diff --git a/launcher/InstanceCopyPrefs.cpp b/launcher/InstanceCopyPrefs.cpp index ae30bb824..7b93a5164 100644 --- a/launcher/InstanceCopyPrefs.cpp +++ b/launcher/InstanceCopyPrefs.cpp @@ -12,7 +12,8 @@ bool InstanceCopyPrefs::allTrue() const copyResourcePacks && copyShaderPacks && copyServers && - copyMods; + copyMods && + copyScreenshots; } // Returns a single RegEx string of the selected folders/files to filter out (ex: ".minecraft/saves|.minecraft/server.dat") @@ -38,6 +39,9 @@ QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const if(!copyMods) filters << "coremods" << "mods" << "config"; + if(!copyScreenshots) + filters << "screenshots"; + // If we have any filters to add, join them as a single regex string to return: if (!filters.isEmpty()) { const QString MC_ROOT = "[.]?minecraft/"; @@ -84,6 +88,11 @@ bool InstanceCopyPrefs::isCopyModsEnabled() const return copyMods; } +bool InstanceCopyPrefs::isCopyScreenshotsEnabled() const +{ + return copyScreenshots; +} + // ======= Setters ======= void InstanceCopyPrefs::enableCopySaves(bool b) { @@ -119,3 +128,8 @@ void InstanceCopyPrefs::enableCopyMods(bool b) { copyMods = b; } + +void InstanceCopyPrefs::enableCopyScreenshots(bool b) +{ + copyScreenshots = b; +} diff --git a/launcher/InstanceCopyPrefs.h b/launcher/InstanceCopyPrefs.h index 3855965de..6988b2df3 100644 --- a/launcher/InstanceCopyPrefs.h +++ b/launcher/InstanceCopyPrefs.h @@ -18,6 +18,7 @@ struct InstanceCopyPrefs { [[nodiscard]] bool isCopyShaderPacksEnabled() const; [[nodiscard]] bool isCopyServersEnabled() const; [[nodiscard]] bool isCopyModsEnabled() const; + [[nodiscard]] bool isCopyScreenshotsEnabled() const; // Setters void enableCopySaves(bool b); void enableKeepPlaytime(bool b); @@ -26,6 +27,7 @@ struct InstanceCopyPrefs { void enableCopyShaderPacks(bool b); void enableCopyServers(bool b); void enableCopyMods(bool b); + void enableCopyScreenshots(bool b); protected: // data bool copySaves = true; @@ -35,4 +37,5 @@ struct InstanceCopyPrefs { bool copyShaderPacks = true; bool copyServers = true; bool copyMods = true; + bool copyScreenshots = true; }; diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index f76b509ea..3f5122f66 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -84,6 +84,7 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent) ui->copyShaderPacksCheckbox->setChecked(m_selectedOptions.isCopyShaderPacksEnabled()); ui->copyServersCheckbox->setChecked(m_selectedOptions.isCopyServersEnabled()); ui->copyModsCheckbox->setChecked(m_selectedOptions.isCopyModsEnabled()); + ui->copyScreenshotsCheckbox->setChecked(m_selectedOptions.isCopyScreenshotsEnabled()); } CopyInstanceDialog::~CopyInstanceDialog() @@ -135,6 +136,7 @@ void CopyInstanceDialog::checkAllCheckboxes(const bool& b) ui->copyShaderPacksCheckbox->setChecked(b); ui->copyServersCheckbox->setChecked(b); ui->copyModsCheckbox->setChecked(b); + ui->copyScreenshotsCheckbox->setChecked(b); } // Check the "Select all" checkbox if all options are already selected: @@ -212,3 +214,9 @@ void CopyInstanceDialog::on_copyModsCheckbox_stateChanged(int state) m_selectedOptions.enableCopyMods(state == Qt::Checked); updateSelectAllCheckbox(); } + +void CopyInstanceDialog::on_copyScreenshotsCheckbox_stateChanged(int state) +{ + m_selectedOptions.enableCopyScreenshots(state == Qt::Checked); + updateSelectAllCheckbox(); +} diff --git a/launcher/ui/dialogs/CopyInstanceDialog.h b/launcher/ui/dialogs/CopyInstanceDialog.h index 94015334e..884501d14 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.h +++ b/launcher/ui/dialogs/CopyInstanceDialog.h @@ -54,6 +54,7 @@ slots: void on_copyShaderPacksCheckbox_stateChanged(int state); void on_copyServersCheckbox_stateChanged(int state); void on_copyModsCheckbox_stateChanged(int state); + void on_copyScreenshotsCheckbox_stateChanged(int state); private: void checkAllCheckboxes(const bool& b); diff --git a/launcher/ui/dialogs/CopyInstanceDialog.ui b/launcher/ui/dialogs/CopyInstanceDialog.ui index 822ed7977..b7828fe31 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.ui +++ b/launcher/ui/dialogs/CopyInstanceDialog.ui @@ -10,7 +10,7 @@ 0 0 341 - 385 + 399 @@ -112,25 +112,31 @@ + + + + + + + 0 + 0 + + + + Qt::LeftToRight + + + Select all + + + false + + + + + - - - - Copy saves - - - - - - - true - - - Copy resource packs - - - @@ -151,17 +157,10 @@ - - + + - Copy servers - - - - - - - Keep play time + Copy saves @@ -172,10 +171,34 @@ - - + + - Select all + Copy servers + + + + + + + true + + + Copy resource packs + + + + + + + Keep play time + + + + + + + Copy screenshots From 028e086960402f685e07163def36d6b5eee1b796 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Mon, 24 Oct 2022 04:08:38 -0700 Subject: [PATCH 10/59] send blocked mod info to dialog & prototype UI Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- .../flame/FlameInstanceCreationTask.cpp | 18 +++++--- .../modpacksch/FTBPackInstallTask.cpp | 22 ++++++---- launcher/ui/dialogs/BlockedModsDialog.cpp | 42 +++++++++++++++++-- launcher/ui/dialogs/BlockedModsDialog.h | 16 ++++++- 4 files changed, 78 insertions(+), 20 deletions(-) diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 48ac02e06..15e660a9d 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -372,13 +372,20 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) auto results = m_mod_id_resolver->getResults(); // first check for blocked mods - QString text; - QList urls; + QList blocked_mods; auto anyBlocked = false; for (const auto& result : results.files.values()) { if (!result.resolved || result.url.isEmpty()) { - text += QString("%1: %2
").arg(result.fileName, result.websiteUrl); - urls.append(QUrl(result.websiteUrl)); + + BlockedMod blocked_mod; + blocked_mod.name = result.fileName; + blocked_mod.websiteUrl = result.websiteUrl; + blocked_mod.hash = result.hash; + blocked_mod.matched = false; + blocked_mod.localPath = ""; + + blocked_mods.append(blocked_mod); + anyBlocked = true; } } @@ -388,8 +395,7 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked mods found"), tr("The following mods were blocked on third party launchers.
" "You will need to manually download them and add them to the modpack"), - text, - urls); + blocked_mods); message_dialog->setModal(true); if (message_dialog->exec()) { diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 7b112d8f9..75fda2086 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -176,8 +176,7 @@ void PackInstallTask::resolveMods() void PackInstallTask::onResolveModsSucceeded() { - QString text; - QList urls; + QList blocked_mods; auto anyBlocked = false; Flame::Manifest results = m_mod_id_resolver_task->getResults(); @@ -191,11 +190,19 @@ void PackInstallTask::onResolveModsSucceeded() // First check for blocked mods if (!results_file.resolved || results_file.url.isEmpty()) { - QString type(local_file.type); + // QString type(local_file.type); + + // type[0] = type[0].toUpper(); + + BlockedMod blocked_mod; + blocked_mod.name = local_file.name; + blocked_mod.websiteUrl = results_file.websiteUrl; + blocked_mod.hash = results_file.hash; + blocked_mod.matched = false; + blocked_mod.localPath = ""; + + blocked_mods.append(blocked_mod); - type[0] = type[0].toUpper(); - text += QString("%1: %2 - %3
").arg(type, local_file.name, results_file.websiteUrl); - urls.append(QUrl(results_file.websiteUrl)); anyBlocked = true; } else { local_file.url = results_file.url.toString(); @@ -210,8 +217,7 @@ void PackInstallTask::onResolveModsSucceeded() auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked files found"), tr("The following files are not available for download in third party launchers.
" "You will need to manually download them and add them to the instance."), - text, - urls); + blocked_mods); if (message_dialog->exec() == QDialog::Accepted) createInstance(); diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index fe87b517c..e29f8eb3e 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -4,17 +4,22 @@ #include #include +#include -BlockedModsDialog::BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, const QString &body, const QList &urls) : - QDialog(parent), ui(new Ui::BlockedModsDialog), urls(urls) { + +BlockedModsDialog::BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, const QList &mods) : + QDialog(parent), ui(new Ui::BlockedModsDialog), mods(mods) { ui->setupUi(this); auto openAllButton = ui->buttonBox->addButton(tr("Open All"), QDialogButtonBox::ActionRole); connect(openAllButton, &QPushButton::clicked, this, &BlockedModsDialog::openAll); + qDebug() << "Mods List: " << mods; + this->setWindowTitle(title); ui->label->setText(text); ui->textBrowser->setText(body); + update(); } BlockedModsDialog::~BlockedModsDialog() { @@ -22,7 +27,36 @@ BlockedModsDialog::~BlockedModsDialog() { } void BlockedModsDialog::openAll() { - for(auto &url : urls) { - QDesktopServices::openUrl(url); + for(auto &mod : mods) { + QDesktopServices::openUrl(mod.websiteUrl); } } + +void BlockedModsDialog::update() { + QString text; + QString span; + + for (auto &mod : mods) { + if (mod.matched) { + // ✔ -> html for HEAVY CHECK MARK : ✔ + span = QString(" ✔ Found at %1 ").arg(mod.localPath); + } else { + // ✘ -> html for HEAVY BALLOT X : ✘ + span = QString(" ✘ Not Found "); + } + text += QString("%1: %2

Hash: %3 %4


").arg(mod.name, mod.websiteUrl, mod.hash, span); + } + + ui->textBrowser->setText(text); +} + + +QDebug operator<<(QDebug debug, const BlockedMod &m) { + QDebugStateSaver saver(debug); + + debug.nospace() << "{ name: " << m.name << ", websiteUrl: " << m.websiteUrl + << ", hash: " << m.hash << ", matched: " << m.matched + << ", localPath: " << m.localPath <<"}"; + + return debug; +} \ No newline at end of file diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index 5f5bd61b7..4be020eca 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -3,6 +3,15 @@ #include +struct BlockedMod { + QString name; + QString websiteUrl; + QString hash; + bool matched; + QString localPath; + +}; + QT_BEGIN_NAMESPACE namespace Ui { class BlockedModsDialog; } QT_END_NAMESPACE @@ -11,12 +20,15 @@ class BlockedModsDialog : public QDialog { Q_OBJECT public: - BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, const QString &body, const QList &urls); + BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, const QList &mods); ~BlockedModsDialog() override; + private: Ui::BlockedModsDialog *ui; - const QList &urls; + const QList &mods; void openAll(); + void update(); }; + From 1598d6582473f1bb6aa02fd9b4dabc8210771e56 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Tue, 25 Oct 2022 01:19:19 -0700 Subject: [PATCH 11/59] watch filesystem, compute and match hashes Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/modplatform/helpers/HashUtils.cpp | 58 ++++++++++ launcher/modplatform/helpers/HashUtils.h | 15 +++ launcher/ui/dialogs/BlockedModsDialog.cpp | 120 ++++++++++++++++++++- launcher/ui/dialogs/BlockedModsDialog.h | 23 +++- 4 files changed, 212 insertions(+), 4 deletions(-) diff --git a/launcher/modplatform/helpers/HashUtils.cpp b/launcher/modplatform/helpers/HashUtils.cpp index a7bbaba50..bf53aa0e1 100644 --- a/launcher/modplatform/helpers/HashUtils.cpp +++ b/launcher/modplatform/helpers/HashUtils.cpp @@ -35,6 +35,18 @@ Hasher::Ptr createFlameHasher(QString file_path) return new FlameHasher(file_path); } +Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider) +{ + return new BlockedModHasher(file_path, provider); +} + +Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider, QString type) +{ + auto hasher = new BlockedModHasher(file_path, provider); + hasher->useHashType(type); + return hasher; +} + void ModrinthHasher::executeTask() { QFile file(m_path); @@ -78,4 +90,50 @@ void FlameHasher::executeTask() } } + +BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::Provider provider) + : Hasher(file_path), provider(provider) { + setObjectName(QString("BlockedModHasher: %1").arg(file_path)); + hash_type = ProviderCaps.hashType(provider).first(); +} + +void BlockedModHasher::executeTask() +{ + QFile file(m_path); + + try { + file.open(QFile::ReadOnly); + } catch (FS::FileSystemException& e) { + qCritical() << QString("Failed to open JAR file in %1").arg(m_path); + qCritical() << QString("Reason: ") << e.cause(); + + emitFailed("Failed to open file for hashing."); + return; + } + + m_hash = ProviderCaps.hash(provider, &file, hash_type); + + file.close(); + + if (m_hash.isEmpty()) { + emitFailed("Empty hash!"); + } else { + emitSucceeded(); + } +} + +QStringList BlockedModHasher::getHashTypes() { + return ProviderCaps.hashType(provider); +} + +bool BlockedModHasher::useHashType(QString type) { + auto types = ProviderCaps.hashType(provider); + if (types.contains(type)) { + hash_type = type; + return true; + } + qDebug() << "Bad hash type " << type << " for provider"; + return false; +} + } // namespace Hashing diff --git a/launcher/modplatform/helpers/HashUtils.h b/launcher/modplatform/helpers/HashUtils.h index 38fddf039..fa3244f6b 100644 --- a/launcher/modplatform/helpers/HashUtils.h +++ b/launcher/modplatform/helpers/HashUtils.h @@ -40,8 +40,23 @@ class ModrinthHasher : public Hasher { void executeTask() override; }; +class BlockedModHasher : public Hasher { + public: + BlockedModHasher(QString file_path, ModPlatform::Provider provider); + + void executeTask() override; + + QStringList getHashTypes(); + bool useHashType(QString type); + private: + ModPlatform::Provider provider; + QString hash_type; +}; + Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider); Hasher::Ptr createFlameHasher(QString file_path); Hasher::Ptr createModrinthHasher(QString file_path); +Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider); +Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::Provider provider, QString type); } // namespace Hashing diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index e29f8eb3e..9ba033d79 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -1,3 +1,6 @@ +#include +#include +#include "Application.h" #include "BlockedModsDialog.h" #include "ui_BlockedModsDialog.h" #include @@ -5,20 +8,29 @@ #include #include +#include -BlockedModsDialog::BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, const QList &mods) : + + +BlockedModsDialog::BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, QList &mods) : QDialog(parent), ui(new Ui::BlockedModsDialog), mods(mods) { ui->setupUi(this); auto openAllButton = ui->buttonBox->addButton(tr("Open All"), QDialogButtonBox::ActionRole); connect(openAllButton, &QPushButton::clicked, this, &BlockedModsDialog::openAll); + connect(&watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged); + + hashing_task = shared_qobject_ptr(new ConcurrentTask(this, "MakeHashesTask", 10)); + qDebug() << "Mods List: " << mods; + setupWatch(); + scanPaths(true); + this->setWindowTitle(title); ui->label->setText(text); - ui->textBrowser->setText(body); update(); } @@ -50,6 +62,110 @@ void BlockedModsDialog::update() { ui->textBrowser->setText(text); } +void BlockedModsDialog::directoryChanged(QString path) { + qDebug() << "Directory changed: " << path; + scanPath(path, false); +} + + +void BlockedModsDialog::setupWatch() { + const QString downloadsFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + const QString modsFolder = APPLICATION->settings()->get("CentralModsDir").toString(); + watcher.addPath(downloadsFolder); + watcher.addPath(modsFolder); +} + +void BlockedModsDialog::scanPaths(bool init) { + for (auto &dir : watcher.directories()) { + scanPath(dir, init); + } +} + +void BlockedModsDialog::scanPath(QString path, bool init) { + + QDir scan_dir(path); + QDirIterator scan_it(path, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::NoIteratorFlags); + while (scan_it.hasNext()) { + QString file = scan_it.next(); + + if (checked_paths.contains(file)){ + continue; + } + + if (!checkValidPath(file)) { + continue; + } + + auto hash_task = Hashing::createBlockedModHasher(file, ModPlatform::Provider::FLAME, "sha1"); + + qDebug() << "Creating Hash task for path: " << file; + + connect(hash_task.get(), &Task::succeeded, [this, hash_task, file] { + checkMatchHash(hash_task->getResult(), file); + }); + connect(hash_task.get(), &Task::failed, [this, hash_task, file] { + qDebug() << "Failed to hash path: " << file; + }); + + if (init) { + checked_paths.insert(file); + } + + hashing_task->addTask(hash_task); + } + + hashing_task->start(); + +} + +void BlockedModsDialog::checkMatchHash(QString hash, QString path) { + bool match = false; + + qDebug() << "Checking for match on hash: " << hash << " | From path:" << path; + + for (auto &mod : mods) { + if (mod.matched) { + continue; + } + if (mod.hash.compare(hash, Qt::CaseInsensitive) == 0) { + mod.matched = true; + mod.localPath = path; + match = true; + + qDebug() << "Hash match found: " << mod.name << " " << hash << " | From path:" << path; + + break; + } + } + + if (match) { + update(); + } +} + +bool BlockedModsDialog::checkValidPath(QString path) { + + QFileInfo file = QFileInfo(path); + QString filename = file.fileName(); + + for (auto &mod : mods) { + if (mod.name.compare(filename, Qt::CaseInsensitive) == 0) { + qDebug() << "Name match found: " << mod.name << " | From path:" << path; + return true; + } + } + + return false; +} + +bool BlockedModsDialog::allModsMatched() { + for (auto &mod : mods) { + if (!mod.matched) + return false; + } + return true; +} + QDebug operator<<(QDebug debug, const BlockedMod &m) { QDebugStateSaver saver(debug); diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index 4be020eca..f1ea99ca0 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -1,7 +1,14 @@ #pragma once #include +#include +#include +#include + +#include "modplatform/helpers/HashUtils.h" + +#include "tasks/ConcurrentTask.h" struct BlockedMod { QString name; @@ -20,15 +27,27 @@ class BlockedModsDialog : public QDialog { Q_OBJECT public: - BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, const QList &mods); + BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, QList &mods); ~BlockedModsDialog() override; private: Ui::BlockedModsDialog *ui; - const QList &mods; + QList &mods; + QFileSystemWatcher watcher; + shared_qobject_ptr hashing_task; + QSet checked_paths; + void openAll(); void update(); + void directoryChanged(QString path); + void setupWatch(); + void scanPaths(bool init); + void scanPath(QString path, bool init); + void checkMatchHash(QString hash, QString path); + + bool checkValidPath(QString path); + bool allModsMatched(); }; From 13c7efa0584caf34950a6e6efa4b8e3bee16d764 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Tue, 25 Oct 2022 10:59:37 -0700 Subject: [PATCH 12/59] copy found mods to instance (FTB and Flame) Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/FileSystem.cpp | 22 +++++++++ launcher/FileSystem.h | 2 + .../flame/FlameInstanceCreationTask.cpp | 35 +++++++++++++- .../flame/FlameInstanceCreationTask.h | 3 ++ .../modpacksch/FTBPackInstallTask.cpp | 46 +++++++++++++++++-- .../modpacksch/FTBPackInstallTask.h | 3 ++ launcher/ui/dialogs/BlockedModsDialog.cpp | 7 +++ launcher/ui/dialogs/BlockedModsDialog.h | 1 + launcher/ui/dialogs/BlockedModsDialog.ui | 37 +++++++++------ 9 files changed, 137 insertions(+), 19 deletions(-) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 4026d6c16..4285fa876 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -163,6 +163,28 @@ bool ensureFolderPathExists(QString foldernamepath) return success; } +bool copyFile(QString &src, QString &dst) { + using copy_opts = fs::copy_options; + + std::error_code err; + + fs::copy_options opt = copy_opts::none; + // The default behavior is to follow symlinks + opt |= copy_opts::copy_symlinks; + + ensureFilePathExists(dst); + + fs::copy(toStdString(src), toStdString(dst), opt, err); + if (err) { + qWarning() << "Failed to copy files:" << QString::fromStdString(err.message()); + qDebug() << "Source file:" << src; + qDebug() << "Destination file:" << dst; + } + + return err.value() == 0; + +} + bool copy::operator()(const QString& offset) { using copy_opts = fs::copy_options; diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index b46f32812..68f6bc4c8 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -75,6 +75,8 @@ bool ensureFilePathExists(QString filenamepath); */ bool ensureFolderPathExists(QString filenamepath); +bool copyFile(QString &src, QString &dst); + class copy { public: copy(const QString& src, const QString& dst) diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 15e660a9d..fbc4ecf30 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -399,6 +399,7 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) message_dialog->setModal(true); if (message_dialog->exec()) { + copyBlockedMods(blocked_mods); setupDownloadJob(loop); } else { m_mod_id_resolver.reset(); @@ -410,6 +411,36 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) } } +void FlameCreationTask::copyBlockedMods(QList blocked_mods) { + + setStatus(tr("Copying Blocked Mods...")); + setAbortable(false); + int i = 0; + int total = blocked_mods.length(); + setProgress(i, total); + for (auto mod = blocked_mods.begin(); mod != blocked_mods.end(); mod++, i++) { + + if (!mod->matched) { + qDebug() << mod->name << "was not matched to a local file, skipping copy"; + continue; + } + + auto dest_path = FS::PathCombine(m_stagingPath, "minecraft", "mods", mod->name); + + setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total))); + + qDebug() << "Will try to copy" << mod->localPath << "to" << dest_path; + + if (!FS::copyFile(mod->localPath, dest_path)) { + qDebug() << "Copy of" << mod->localPath << "to" << dest_path << "Failed"; + } + + setProgress(i+1, total); + } + + setAbortable(true); +} + void FlameCreationTask::setupDownloadJob(QEventLoop& loop) { m_files_job = new NetJob(tr("Mod download"), APPLICATION->network()); @@ -455,7 +486,9 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) m_files_job.reset(); setError(reason); }); - connect(m_files_job.get(), &NetJob::progress, [&](qint64 current, qint64 total) { setProgress(current, total); }); + connect(m_files_job.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + setProgress(current, total); + }); connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit); setStatus(tr("Downloading mods...")); diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h index ded0e2ceb..69a8f1ab4 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.h +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -10,6 +10,8 @@ #include "net/NetJob.h" +#include "ui/dialogs/BlockedModsDialog.h" + class FlameCreationTask final : public InstanceCreationTask { Q_OBJECT @@ -29,6 +31,7 @@ class FlameCreationTask final : public InstanceCreationTask { private slots: void idResolverSucceeded(QEventLoop&); void setupDownloadJob(QEventLoop&); + void copyBlockedMods(QList blocked_mods); private: QWidget* m_parent = nullptr; diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 75fda2086..f6bf24880 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -176,7 +176,6 @@ void PackInstallTask::resolveMods() void PackInstallTask::onResolveModsSucceeded() { - QList blocked_mods; auto anyBlocked = false; Flame::Manifest results = m_mod_id_resolver_task->getResults(); @@ -201,7 +200,7 @@ void PackInstallTask::onResolveModsSucceeded() blocked_mod.matched = false; blocked_mod.localPath = ""; - blocked_mods.append(blocked_mod); + m_blocked_mods.append(blocked_mod); anyBlocked = true; } else { @@ -217,12 +216,16 @@ void PackInstallTask::onResolveModsSucceeded() auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked files found"), tr("The following files are not available for download in third party launchers.
" "You will need to manually download them and add them to the instance."), - blocked_mods); + m_blocked_mods); - if (message_dialog->exec() == QDialog::Accepted) + if (message_dialog->exec() == QDialog::Accepted) { + qDebug() << "Post dialog mods list: " << m_blocked_mods; createInstance(); - else + } + else { abort(); + } + } else { createInstance(); } @@ -326,6 +329,9 @@ void PackInstallTask::downloadPack() void PackInstallTask::onModDownloadSucceeded() { m_net_job.reset(); + if (m_blocked_mods.length() > 0) { + copyBlockedMods(); + } emitSucceeded(); } @@ -349,4 +355,34 @@ void PackInstallTask::onModDownloadFailed(QString reason) emitFailed(reason); } +void PackInstallTask::copyBlockedMods() { + + setStatus(tr("Copying Blocked Mods...")); + setAbortable(false); + int i = 0; + int total = m_blocked_mods.length(); + setProgress(i, total); + for (auto mod = m_blocked_mods.begin(); mod != m_blocked_mods.end(); mod++, i++) { + + if (!mod->matched) { + qDebug() << mod->name << "was not matched to a local file, skipping copy"; + continue; + } + + auto dest_path = FS::PathCombine(m_stagingPath, ".minecraft", "mods", mod->name); + + setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total))); + + qDebug() << "Will try to copy" << mod->localPath << "to" << dest_path; + + if (!FS::copyFile(mod->localPath, dest_path)) { + qDebug() << "Copy of" << mod->localPath << "to" << dest_path << "Failed"; + } + + setProgress(i+1, total); + } + + setAbortable(true); +} + } // namespace ModpacksCH diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.h b/launcher/modplatform/modpacksch/FTBPackInstallTask.h index 7c6fbeb93..2cd4d7296 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.h +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.h @@ -43,6 +43,7 @@ #include "QObjectPtr.h" #include "modplatform/flame/FileResolvingTask.h" #include "net/NetJob.h" +#include "ui/dialogs/BlockedModsDialog.h" #include @@ -76,6 +77,7 @@ private: void resolveMods(); void createInstance(); void downloadPack(); + void copyBlockedMods(); private: NetJob::Ptr m_net_job = nullptr; @@ -90,6 +92,7 @@ private: Version m_version; QMap m_files_to_copy; + QList m_blocked_mods; //FIXME: nuke QWidget* m_parent; diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index 9ba033d79..542d06815 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -31,6 +31,7 @@ BlockedModsDialog::BlockedModsDialog(QWidget *parent, const QString &title, cons this->setWindowTitle(title); ui->label->setText(text); + ui->labelModsFound->setText("Please download the missing mods."); update(); } @@ -60,6 +61,12 @@ void BlockedModsDialog::update() { } ui->textBrowser->setText(text); + + if (allModsMatched()) { + ui->labelModsFound->setText("All mods found ✔"); + } else { + ui->labelModsFound->setText("Please download the missing mods."); + } } void BlockedModsDialog::directoryChanged(QString path) { diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index f1ea99ca0..93b9f46a6 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -51,3 +51,4 @@ private: bool allModsMatched(); }; +QDebug operator<<(QDebug debug, const BlockedMod &m); \ No newline at end of file diff --git a/launcher/ui/dialogs/BlockedModsDialog.ui b/launcher/ui/dialogs/BlockedModsDialog.ui index f4ae95b69..371549cf4 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.ui +++ b/launcher/ui/dialogs/BlockedModsDialog.ui @@ -13,8 +13,8 @@ BlockedModsDialog - - + + @@ -24,17 +24,7 @@ - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - + true @@ -44,6 +34,27 @@ + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + +
From e9d4793b1e98944dad910b3952c117bb2d3369de Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Tue, 25 Oct 2022 20:18:14 -0700 Subject: [PATCH 13/59] minor clean up and add some docs Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/FileSystem.cpp | 7 ++++ launcher/FileSystem.h | 1 + .../flame/FlameInstanceCreationTask.cpp | 2 + .../modpacksch/FTBPackInstallTask.cpp | 4 +- launcher/ui/dialogs/BlockedModsDialog.cpp | 42 +++++++++++-------- launcher/ui/dialogs/BlockedModsDialog.h | 5 +-- 6 files changed, 38 insertions(+), 23 deletions(-) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 4285fa876..8fe441b31 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -163,6 +163,10 @@ bool ensureFolderPathExists(QString foldernamepath) return success; } +/// @brief Copy file at src to dest, ensures the full filepath exsists +/// @param src srouce file path +/// @param dst destination file path +/// @return boolean: was there an error during the filecopy? bool copyFile(QString &src, QString &dst) { using copy_opts = fs::copy_options; @@ -185,6 +189,9 @@ bool copyFile(QString &src, QString &dst) { } +/// @brief Copies a directory and it's contents from src to dest +/// @param offset subdirectory form src to copy to dest +/// @return if there was an error during the filecopy bool copy::operator()(const QString& offset) { using copy_opts = fs::copy_options; diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index 68f6bc4c8..ab006d48a 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -77,6 +77,7 @@ bool ensureFolderPathExists(QString filenamepath); bool copyFile(QString &src, QString &dst); +/// @brief Copies a directory and it's contents from src to dest class copy { public: copy(const QString& src, const QString& dst) diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index fbc4ecf30..edacb8197 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -411,6 +411,8 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) } } +/// @brief copy the matched blocked mods to the instance staging area +/// @param blocked_mods list of the blocked mods and their matched paths void FlameCreationTask::copyBlockedMods(QList blocked_mods) { setStatus(tr("Copying Blocked Mods...")); diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index f6bf24880..49fbafd67 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -189,9 +189,6 @@ void PackInstallTask::onResolveModsSucceeded() // First check for blocked mods if (!results_file.resolved || results_file.url.isEmpty()) { - // QString type(local_file.type); - - // type[0] = type[0].toUpper(); BlockedMod blocked_mod; blocked_mod.name = local_file.name; @@ -355,6 +352,7 @@ void PackInstallTask::onModDownloadFailed(QString reason) emitFailed(reason); } +/// @brief copy the matched blocked mods to the instance staging area void PackInstallTask::copyBlockedMods() { setStatus(tr("Copying Blocked Mods...")); diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index 542d06815..f5bc7f735 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -1,5 +1,3 @@ -#include -#include #include "Application.h" #include "BlockedModsDialog.h" #include "ui_BlockedModsDialog.h" @@ -27,7 +25,7 @@ BlockedModsDialog::BlockedModsDialog(QWidget *parent, const QString &title, cons qDebug() << "Mods List: " << mods; setupWatch(); - scanPaths(true); + scanPaths(); this->setWindowTitle(title); ui->label->setText(text); @@ -45,6 +43,7 @@ void BlockedModsDialog::openAll() { } } +/// @brief update UI with current status of the blocked mod detection void BlockedModsDialog::update() { QString text; QString span; @@ -69,12 +68,15 @@ void BlockedModsDialog::update() { } } +/// @brief Signal fired when a watched direcotry has changed +/// @param path the path to the changed directory void BlockedModsDialog::directoryChanged(QString path) { qDebug() << "Directory changed: " << path; - scanPath(path, false); + scanPath(path); } +/// @brief add the user downloads folder and the global mods folder to the filesystem watcher void BlockedModsDialog::setupWatch() { const QString downloadsFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); const QString modsFolder = APPLICATION->settings()->get("CentralModsDir").toString(); @@ -82,23 +84,24 @@ void BlockedModsDialog::setupWatch() { watcher.addPath(modsFolder); } -void BlockedModsDialog::scanPaths(bool init) { + +/// @brief scan all watched folder +void BlockedModsDialog::scanPaths() { for (auto &dir : watcher.directories()) { - scanPath(dir, init); + scanPath(dir); } } -void BlockedModsDialog::scanPath(QString path, bool init) { +/// @brief Scan the directory at path, skip paths that do not contain a file name +/// of a blocked mod we are looking for +/// @param path the directory to scan +void BlockedModsDialog::scanPath(QString path) { QDir scan_dir(path); QDirIterator scan_it(path, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::NoIteratorFlags); while (scan_it.hasNext()) { QString file = scan_it.next(); - if (checked_paths.contains(file)){ - continue; - } - if (!checkValidPath(file)) { continue; } @@ -113,10 +116,6 @@ void BlockedModsDialog::scanPath(QString path, bool init) { connect(hash_task.get(), &Task::failed, [this, hash_task, file] { qDebug() << "Failed to hash path: " << file; }); - - if (init) { - checked_paths.insert(file); - } hashing_task->addTask(hash_task); } @@ -125,6 +124,10 @@ void BlockedModsDialog::scanPath(QString path, bool init) { } +/// @brief check if the conputed hash for the provided path matches a blocked +/// mod we are looking for +/// @param hash the computed hash for the provided path +/// @param path the path to the local file being compared void BlockedModsDialog::checkMatchHash(QString hash, QString path) { bool match = false; @@ -150,6 +153,9 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path) { } } +/// @brief Check if the name of the file at path matches the naem of a blocked mod we are searching for +/// @param path the path to check +/// @return boolean: did the path match the name of a blocked mod? bool BlockedModsDialog::checkValidPath(QString path) { QFileInfo file = QFileInfo(path); @@ -165,6 +171,8 @@ bool BlockedModsDialog::checkValidPath(QString path) { return false; } +/// @brief have we found all the mods we're lookign for? +/// @return boolean bool BlockedModsDialog::allModsMatched() { for (auto &mod : mods) { if (!mod.matched) @@ -173,7 +181,7 @@ bool BlockedModsDialog::allModsMatched() { return true; } - +/// qDebug print support for the BlockedMod struct QDebug operator<<(QDebug debug, const BlockedMod &m) { QDebugStateSaver saver(debug); @@ -182,4 +190,4 @@ QDebug operator<<(QDebug debug, const BlockedMod &m) { << ", localPath: " << m.localPath <<"}"; return debug; -} \ No newline at end of file +} diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index 93b9f46a6..cf1d3b3d5 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -37,14 +37,13 @@ private: QList &mods; QFileSystemWatcher watcher; shared_qobject_ptr hashing_task; - QSet checked_paths; void openAll(); void update(); void directoryChanged(QString path); void setupWatch(); - void scanPaths(bool init); - void scanPath(QString path, bool init); + void scanPaths(); + void scanPath(QString path); void checkMatchHash(QString hash, QString path); bool checkValidPath(QString path); From d2f3dbaa2984b70a71e5fb1b246a31987a6fdf10 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sun, 30 Oct 2022 22:39:12 -0700 Subject: [PATCH 14/59] fix mispellings and wrap strings for translation Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/ui/dialogs/BlockedModsDialog.cpp | 18 ++++++++---------- launcher/ui/dialogs/BlockedModsDialog.h | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index f5bc7f735..136a7371b 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -29,7 +29,7 @@ BlockedModsDialog::BlockedModsDialog(QWidget *parent, const QString &title, cons this->setWindowTitle(title); ui->label->setText(text); - ui->labelModsFound->setText("Please download the missing mods."); + ui->labelModsFound->setText(tr("Please download the missing mods.")); update(); } @@ -51,20 +51,20 @@ void BlockedModsDialog::update() { for (auto &mod : mods) { if (mod.matched) { // ✔ -> html for HEAVY CHECK MARK : ✔ - span = QString(" ✔ Found at %1 ").arg(mod.localPath); + span = QString(tr(" ✔ Found at %1 ")).arg(mod.localPath); } else { // ✘ -> html for HEAVY BALLOT X : ✘ - span = QString(" ✘ Not Found "); + span = QString(tr(" ✘ Not Found ")); } - text += QString("%1: %2

Hash: %3 %4


").arg(mod.name, mod.websiteUrl, mod.hash, span); + text += QString(tr("%1: %2

Hash: %3 %4


")).arg(mod.name, mod.websiteUrl, mod.hash, span); } ui->textBrowser->setText(text); if (allModsMatched()) { - ui->labelModsFound->setText("All mods found ✔"); + ui->labelModsFound->setText(tr("All mods found ✔")); } else { - ui->labelModsFound->setText("Please download the missing mods."); + ui->labelModsFound->setText(tr("Please download the missing mods.")); } } @@ -124,7 +124,7 @@ void BlockedModsDialog::scanPath(QString path) { } -/// @brief check if the conputed hash for the provided path matches a blocked +/// @brief check if the computed hash for the provided path matches a blocked /// mod we are looking for /// @param hash the computed hash for the provided path /// @param path the path to the local file being compared @@ -153,7 +153,7 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path) { } } -/// @brief Check if the name of the file at path matches the naem of a blocked mod we are searching for +/// @brief Check if the name of the file at path matches the name of a blocked mod we are searching for /// @param path the path to check /// @return boolean: did the path match the name of a blocked mod? bool BlockedModsDialog::checkValidPath(QString path) { @@ -171,8 +171,6 @@ bool BlockedModsDialog::checkValidPath(QString path) { return false; } -/// @brief have we found all the mods we're lookign for? -/// @return boolean bool BlockedModsDialog::allModsMatched() { for (auto &mod : mods) { if (!mod.matched) diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index cf1d3b3d5..0a5c90db0 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -50,4 +50,4 @@ private: bool allModsMatched(); }; -QDebug operator<<(QDebug debug, const BlockedMod &m); \ No newline at end of file +QDebug operator<<(QDebug debug, const BlockedMod &m); From fda2c116bef33df2ca49d77ff4b016e724f47549 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sun, 30 Oct 2022 22:42:35 -0700 Subject: [PATCH 15/59] code quality cleanup Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/FileSystem.cpp | 2 +- launcher/FileSystem.h | 2 +- .../flame/FlameInstanceCreationTask.cpp | 23 +++++++++---------- .../flame/FlameInstanceCreationTask.h | 2 +- .../modpacksch/FTBPackInstallTask.cpp | 2 +- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 8fe441b31..508da08d4 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -167,7 +167,7 @@ bool ensureFolderPathExists(QString foldernamepath) /// @param src srouce file path /// @param dst destination file path /// @return boolean: was there an error during the filecopy? -bool copyFile(QString &src, QString &dst) { +bool copyFile(QString const& src, QString const& dst) { using copy_opts = fs::copy_options; std::error_code err; diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index ab006d48a..771bda605 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -75,7 +75,7 @@ bool ensureFilePathExists(QString filenamepath); */ bool ensureFolderPathExists(QString filenamepath); -bool copyFile(QString &src, QString &dst); +bool copyFile(QString const& src, QString const& dst); /// @brief Copies a directory and it's contents from src to dest class copy { diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index edacb8197..30438a1a5 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -413,31 +413,32 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) /// @brief copy the matched blocked mods to the instance staging area /// @param blocked_mods list of the blocked mods and their matched paths -void FlameCreationTask::copyBlockedMods(QList blocked_mods) { +void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) { setStatus(tr("Copying Blocked Mods...")); setAbortable(false); int i = 0; int total = blocked_mods.length(); setProgress(i, total); - for (auto mod = blocked_mods.begin(); mod != blocked_mods.end(); mod++, i++) { + for (auto &mod : blocked_mods) { - if (!mod->matched) { - qDebug() << mod->name << "was not matched to a local file, skipping copy"; + if (!mod.matched) { + qDebug() << mod.name << "was not matched to a local file, skipping copy"; continue; } - auto dest_path = FS::PathCombine(m_stagingPath, "minecraft", "mods", mod->name); + auto dest_path = FS::PathCombine(m_stagingPath, "minecraft", "mods", mod.name); setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total))); - qDebug() << "Will try to copy" << mod->localPath << "to" << dest_path; + qDebug() << "Will try to copy" << mod.localPath << "to" << dest_path; - if (!FS::copyFile(mod->localPath, dest_path)) { - qDebug() << "Copy of" << mod->localPath << "to" << dest_path << "Failed"; + if (!FS::copyFile(mod.localPath, dest_path)) { // FIXME: use FS::copy once #333 is merged + qDebug() << "Copy of" << mod.localPath << "to" << dest_path << "Failed"; } - setProgress(i+1, total); + i++; + setProgress(i, total); } setAbortable(true); @@ -488,9 +489,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) m_files_job.reset(); setError(reason); }); - connect(m_files_job.get(), &NetJob::progress, [&](qint64 current, qint64 total) { - setProgress(current, total); - }); + connect(m_files_job.get(), &NetJob::progress, this, &FlameCreationTask::setProgress); connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit); setStatus(tr("Downloading mods...")); diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h index 69a8f1ab4..fbc7d5bf7 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.h +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -31,7 +31,7 @@ class FlameCreationTask final : public InstanceCreationTask { private slots: void idResolverSucceeded(QEventLoop&); void setupDownloadJob(QEventLoop&); - void copyBlockedMods(QList blocked_mods); + void copyBlockedMods(QList const& blocked_mods); private: QWidget* m_parent = nullptr; diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 49fbafd67..5091db0ca 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -326,7 +326,7 @@ void PackInstallTask::downloadPack() void PackInstallTask::onModDownloadSucceeded() { m_net_job.reset(); - if (m_blocked_mods.length() > 0) { + if (!m_blocked_mods.isEmpty()) { copyBlockedMods(); } emitSucceeded(); From a7a331a26e43df3dbafdbb29a59d38ba807ffa7d Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sun, 30 Oct 2022 22:49:54 -0700 Subject: [PATCH 16/59] ensure FS::copyFile is marked for removal Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/FileSystem.h | 1 + launcher/modplatform/modpacksch/FTBPackInstallTask.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index 771bda605..11981f68f 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -75,6 +75,7 @@ bool ensureFilePathExists(QString filenamepath); */ bool ensureFolderPathExists(QString filenamepath); +// TODO: remove in favor of FS::copy once #333 is merged bool copyFile(QString const& src, QString const& dst); /// @brief Copies a directory and it's contents from src to dest diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 5091db0ca..06ef1deb6 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -373,7 +373,7 @@ void PackInstallTask::copyBlockedMods() { qDebug() << "Will try to copy" << mod->localPath << "to" << dest_path; - if (!FS::copyFile(mod->localPath, dest_path)) { + if (!FS::copyFile(mod->localPath, dest_path)) { // FIXME: use FS::copy once #333 is merged qDebug() << "Copy of" << mod->localPath << "to" << dest_path << "Failed"; } From 6010ce0dc587527caa05bdc9b4cecdb9bd811375 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Tue, 1 Nov 2022 04:28:57 -0700 Subject: [PATCH 17/59] chore(remove FS::copyFile): Now that #333 is merged and FS::copy works on non directory copyFile can be removed. Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/FileSystem.cpp | 26 ------------------- launcher/FileSystem.h | 3 --- .../flame/FlameInstanceCreationTask.cpp | 2 +- .../modpacksch/FTBPackInstallTask.cpp | 2 +- 4 files changed, 2 insertions(+), 31 deletions(-) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 508da08d4..bf0849ec3 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -163,32 +163,6 @@ bool ensureFolderPathExists(QString foldernamepath) return success; } -/// @brief Copy file at src to dest, ensures the full filepath exsists -/// @param src srouce file path -/// @param dst destination file path -/// @return boolean: was there an error during the filecopy? -bool copyFile(QString const& src, QString const& dst) { - using copy_opts = fs::copy_options; - - std::error_code err; - - fs::copy_options opt = copy_opts::none; - // The default behavior is to follow symlinks - opt |= copy_opts::copy_symlinks; - - ensureFilePathExists(dst); - - fs::copy(toStdString(src), toStdString(dst), opt, err); - if (err) { - qWarning() << "Failed to copy files:" << QString::fromStdString(err.message()); - qDebug() << "Source file:" << src; - qDebug() << "Destination file:" << dst; - } - - return err.value() == 0; - -} - /// @brief Copies a directory and it's contents from src to dest /// @param offset subdirectory form src to copy to dest /// @return if there was an error during the filecopy diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index 11981f68f..b7e175fdf 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -75,9 +75,6 @@ bool ensureFilePathExists(QString filenamepath); */ bool ensureFolderPathExists(QString filenamepath); -// TODO: remove in favor of FS::copy once #333 is merged -bool copyFile(QString const& src, QString const& dst); - /// @brief Copies a directory and it's contents from src to dest class copy { public: diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 30438a1a5..5d4dc689f 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -433,7 +433,7 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) { qDebug() << "Will try to copy" << mod.localPath << "to" << dest_path; - if (!FS::copyFile(mod.localPath, dest_path)) { // FIXME: use FS::copy once #333 is merged + if (!FS::copy(mod.localPath, dest_path)()) { qDebug() << "Copy of" << mod.localPath << "to" << dest_path << "Failed"; } diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 06ef1deb6..1e4bbe19b 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -373,7 +373,7 @@ void PackInstallTask::copyBlockedMods() { qDebug() << "Will try to copy" << mod->localPath << "to" << dest_path; - if (!FS::copyFile(mod->localPath, dest_path)) { // FIXME: use FS::copy once #333 is merged + if (!FS::copy(mod->localPath, dest_path)()) { qDebug() << "Copy of" << mod->localPath << "to" << dest_path << "Failed"; } From 209a1650e489e21417ce2e1a29862703d51a2cd0 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Tue, 1 Nov 2022 07:06:36 -0700 Subject: [PATCH 18/59] clang_format and code cleanup Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- .../flame/FlameInstanceCreationTask.cpp | 11 +-- .../modpacksch/FTBPackInstallTask.cpp | 24 ++--- launcher/ui/dialogs/BlockedModsDialog.cpp | 95 +++++++++---------- 3 files changed, 62 insertions(+), 68 deletions(-) diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 5d4dc689f..f86e9335d 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -413,15 +413,14 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) /// @brief copy the matched blocked mods to the instance staging area /// @param blocked_mods list of the blocked mods and their matched paths -void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) { - +void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) +{ setStatus(tr("Copying Blocked Mods...")); setAbortable(false); int i = 0; int total = blocked_mods.length(); setProgress(i, total); - for (auto &mod : blocked_mods) { - + for (auto const& mod : blocked_mods) { if (!mod.matched) { qDebug() << mod.name << "was not matched to a local file, skipping copy"; continue; @@ -433,9 +432,9 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) { qDebug() << "Will try to copy" << mod.localPath << "to" << dest_path; - if (!FS::copy(mod.localPath, dest_path)()) { + if (!FS::copy(mod.localPath, dest_path)()) { qDebug() << "Copy of" << mod.localPath << "to" << dest_path << "Failed"; - } + } i++; setProgress(i, total); diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 1e4bbe19b..70ef75716 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -353,31 +353,31 @@ void PackInstallTask::onModDownloadFailed(QString reason) } /// @brief copy the matched blocked mods to the instance staging area -void PackInstallTask::copyBlockedMods() { - +void PackInstallTask::copyBlockedMods() +{ setStatus(tr("Copying Blocked Mods...")); setAbortable(false); int i = 0; int total = m_blocked_mods.length(); setProgress(i, total); - for (auto mod = m_blocked_mods.begin(); mod != m_blocked_mods.end(); mod++, i++) { - - if (!mod->matched) { - qDebug() << mod->name << "was not matched to a local file, skipping copy"; + for (auto const& mod : m_blocked_mods) { + if (!mod.matched) { + qDebug() << mod.name << "was not matched to a local file, skipping copy"; continue; } - auto dest_path = FS::PathCombine(m_stagingPath, ".minecraft", "mods", mod->name); + auto dest_path = FS::PathCombine(m_stagingPath, ".minecraft", "mods", mod.name); setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total))); - qDebug() << "Will try to copy" << mod->localPath << "to" << dest_path; + qDebug() << "Will try to copy" << mod.localPath << "to" << dest_path; - if (!FS::copy(mod->localPath, dest_path)()) { - qDebug() << "Copy of" << mod->localPath << "to" << dest_path << "Failed"; - } + if (!FS::copy(mod.localPath, dest_path)()) { + qDebug() << "Copy of" << mod.localPath << "to" << dest_path << "Failed"; + } - setProgress(i+1, total); + i++; + setProgress(i, total); } setAbortable(true); diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index 136a7371b..2cf942500 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -1,18 +1,16 @@ -#include "Application.h" #include "BlockedModsDialog.h" -#include "ui_BlockedModsDialog.h" -#include -#include #include +#include +#include +#include "Application.h" +#include "ui_BlockedModsDialog.h" #include #include - - - -BlockedModsDialog::BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, QList &mods) : - QDialog(parent), ui(new Ui::BlockedModsDialog), mods(mods) { +BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList& mods) + : QDialog(parent), ui(new Ui::BlockedModsDialog), mods(mods) +{ ui->setupUi(this); auto openAllButton = ui->buttonBox->addButton(tr("Open All"), QDialogButtonBox::ActionRole); @@ -21,7 +19,7 @@ BlockedModsDialog::BlockedModsDialog(QWidget *parent, const QString &title, cons connect(&watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged); hashing_task = shared_qobject_ptr(new ConcurrentTask(this, "MakeHashesTask", 10)); - + qDebug() << "Mods List: " << mods; setupWatch(); @@ -33,22 +31,25 @@ BlockedModsDialog::BlockedModsDialog(QWidget *parent, const QString &title, cons update(); } -BlockedModsDialog::~BlockedModsDialog() { +BlockedModsDialog::~BlockedModsDialog() +{ delete ui; } -void BlockedModsDialog::openAll() { - for(auto &mod : mods) { +void BlockedModsDialog::openAll() +{ + for (auto& mod : mods) { QDesktopServices::openUrl(mod.websiteUrl); } } /// @brief update UI with current status of the blocked mod detection -void BlockedModsDialog::update() { +void BlockedModsDialog::update() +{ QString text; QString span; - for (auto &mod : mods) { + for (auto& mod : mods) { if (mod.matched) { // ✔ -> html for HEAVY CHECK MARK : ✔ span = QString(tr(" ✔ Found at %1 ")).arg(mod.localPath); @@ -70,33 +71,34 @@ void BlockedModsDialog::update() { /// @brief Signal fired when a watched direcotry has changed /// @param path the path to the changed directory -void BlockedModsDialog::directoryChanged(QString path) { +void BlockedModsDialog::directoryChanged(QString path) +{ qDebug() << "Directory changed: " << path; scanPath(path); } - /// @brief add the user downloads folder and the global mods folder to the filesystem watcher -void BlockedModsDialog::setupWatch() { +void BlockedModsDialog::setupWatch() +{ const QString downloadsFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); const QString modsFolder = APPLICATION->settings()->get("CentralModsDir").toString(); watcher.addPath(downloadsFolder); watcher.addPath(modsFolder); } - /// @brief scan all watched folder -void BlockedModsDialog::scanPaths() { - for (auto &dir : watcher.directories()) { +void BlockedModsDialog::scanPaths() +{ + for (auto& dir : watcher.directories()) { scanPath(dir); } } -/// @brief Scan the directory at path, skip paths that do not contain a file name +/// @brief Scan the directory at path, skip paths that do not contain a file name /// of a blocked mod we are looking for /// @param path the directory to scan -void BlockedModsDialog::scanPath(QString path) { - +void BlockedModsDialog::scanPath(QString path) +{ QDir scan_dir(path); QDirIterator scan_it(path, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::NoIteratorFlags); while (scan_it.hasNext()) { @@ -110,30 +112,26 @@ void BlockedModsDialog::scanPath(QString path) { qDebug() << "Creating Hash task for path: " << file; - connect(hash_task.get(), &Task::succeeded, [this, hash_task, file] { - checkMatchHash(hash_task->getResult(), file); - }); - connect(hash_task.get(), &Task::failed, [this, hash_task, file] { - qDebug() << "Failed to hash path: " << file; - }); - + connect(hash_task.get(), &Task::succeeded, [this, hash_task, file] { checkMatchHash(hash_task->getResult(), file); }); + connect(hash_task.get(), &Task::failed, [file] { qDebug() << "Failed to hash path: " << file; }); + hashing_task->addTask(hash_task); } hashing_task->start(); - } /// @brief check if the computed hash for the provided path matches a blocked /// mod we are looking for /// @param hash the computed hash for the provided path /// @param path the path to the local file being compared -void BlockedModsDialog::checkMatchHash(QString hash, QString path) { +void BlockedModsDialog::checkMatchHash(QString hash, QString path) +{ bool match = false; - qDebug() << "Checking for match on hash: " << hash << " | From path:" << path; + qDebug() << "Checking for match on hash: " << hash << "| From path:" << path; - for (auto &mod : mods) { + for (auto& mod : mods) { if (mod.matched) { continue; } @@ -142,7 +140,7 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path) { mod.localPath = path; match = true; - qDebug() << "Hash match found: " << mod.name << " " << hash << " | From path:" << path; + qDebug() << "Hash match found:" << mod.name << hash << "| From path:" << path; break; } @@ -156,14 +154,14 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path) { /// @brief Check if the name of the file at path matches the name of a blocked mod we are searching for /// @param path the path to check /// @return boolean: did the path match the name of a blocked mod? -bool BlockedModsDialog::checkValidPath(QString path) { - +bool BlockedModsDialog::checkValidPath(QString path) +{ QFileInfo file = QFileInfo(path); QString filename = file.fileName(); - for (auto &mod : mods) { + for (auto& mod : mods) { if (mod.name.compare(filename, Qt::CaseInsensitive) == 0) { - qDebug() << "Name match found: " << mod.name << " | From path:" << path; + qDebug() << "Name match found:" << mod.name << "| From path:" << path; return true; } } @@ -171,21 +169,18 @@ bool BlockedModsDialog::checkValidPath(QString path) { return false; } -bool BlockedModsDialog::allModsMatched() { - for (auto &mod : mods) { - if (!mod.matched) - return false; - } - return true; +bool BlockedModsDialog::allModsMatched() +{ + return std::all_of(mods.begin(), mods.end(), [](auto const& mod) { return mod.matched; }); } /// qDebug print support for the BlockedMod struct -QDebug operator<<(QDebug debug, const BlockedMod &m) { +QDebug operator<<(QDebug debug, const BlockedMod& m) +{ QDebugStateSaver saver(debug); - debug.nospace() << "{ name: " << m.name << ", websiteUrl: " << m.websiteUrl - << ", hash: " << m.hash << ", matched: " << m.matched - << ", localPath: " << m.localPath <<"}"; + debug.nospace() << "{ name: " << m.name << ", websiteUrl: " << m.websiteUrl << ", hash: " << m.hash << ", matched: " << m.matched + << ", localPath: " << m.localPath << "}"; return debug; } From 7f32c6464d84181fc8947f632da340a863dc53d6 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 1 Nov 2022 16:58:22 +0000 Subject: [PATCH 19/59] Initial better mod browser link implementation Signed-off-by: TheKodeToad --- launcher/ui/dialogs/ModDownloadDialog.cpp | 5 ++ launcher/ui/dialogs/ModDownloadDialog.h | 2 + launcher/ui/pages/modplatform/ModPage.cpp | 63 +++++++++++++++++-- launcher/ui/pages/modplatform/ModPage.h | 1 + launcher/ui/pages/modplatform/ModPage.ui | 4 +- .../pages/modplatform/flame/FlameModPage.cpp | 19 +++++- .../ui/pages/modplatform/flame/FlameModPage.h | 2 + .../modplatform/modrinth/ModrinthModPage.cpp | 2 +- 8 files changed, 87 insertions(+), 11 deletions(-) diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index d740c8cbb..38b4ffcf9 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -187,3 +187,8 @@ void ModDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* select // Same effect as having a global search bar selected_page->setSearchTerm(prev_page->getSearchTerm()); } + +bool ModDownloadDialog::selectPage(QString pageId) +{ + return m_container->selectPage(pageId); +} \ No newline at end of file diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index 18a5f0f36..125cb776a 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -53,6 +53,8 @@ public: const QList getTasks(); const std::shared_ptr &mods; + bool selectPage(QString pageId); + public slots: void confirm(); void accept() override; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index f2c1746fc..7f62fff1e 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad * * 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 @@ -37,6 +38,7 @@ #include "Application.h" #include "ui_ModPage.h" +#include #include #include @@ -80,6 +82,8 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) ui->packView->setItemDelegate(new ProjectItemDelegate(this)); ui->packView->installEventFilter(this); + + connect(ui->packDescription, &QTextBrowser::anchorClicked, this, &ModPage::openUrl); } ModPage::~ModPage() @@ -158,8 +162,8 @@ void ModPage::triggerSearch() { auto changed = m_filter_widget->changed(); m_filter = m_filter_widget->getFilter(); - - if(changed){ + + if (changed) { ui->packView->clearSelection(); ui->packDescription->clear(); ui->versionSelectionBox->clear(); @@ -241,6 +245,54 @@ void ModPage::onModSelected() ui->packView->adjustSize(); } +void ModPage::openUrl(const QUrl& url) +{ + // do not allow other url schemes for security reasons + if (!(url.scheme() == "http" || url.scheme() == "https")) { + qWarning() << "Unsupported scheme" << url.scheme(); + return; + } + + // detect mod URLs and search instead + int prefixLength; + const char* page; + + if ((url.host() == "modrinth.com" || url.host() == "www.modrinth.com") + && url.path().startsWith("/mod/")) { + prefixLength = 5; + page = "modrinth"; + } + else if (APPLICATION->capabilities() & Application::SupportsFlame + && url.host() == "www.curseforge.com" + && url.path().toLower().startsWith("/minecraft/mc-mods/")) { + prefixLength = 19; + page = "curseforge"; + } + else + prefixLength = 0; + + if (prefixLength != 0) { + QString slug = url.path().mid(prefixLength); + + // remove trailing slash(es) + while (slug.endsWith('/')) + slug.remove(slug.length() - 1, 1); + + // ensure that the path doesn't contain any further slashes, + // and the user isn't opening the same mod; they probably + // intended to view in their web browser + if (!slug.isEmpty() && !slug.contains('/') && slug != current.slug) { + ui->searchEdit->setText(slug); + dialog->selectPage(page); + triggerSearch(); + return; + } + } + + // open in the user's web browser + QDesktopServices::openUrl(url); +} + /******** Make changes to the UI ********/ @@ -270,8 +322,8 @@ void ModPage::updateModVersions(int prev_count) if ((valid || m_filter->versions.empty()) && !optedOut(version)) ui->versionSelectionBox->addItem(version.version, QVariant(i)); } - if (ui->versionSelectionBox->count() == 0 && prev_count != 0) { - ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); + if (ui->versionSelectionBox->count() == 0 && prev_count != 0) { + ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); } @@ -317,8 +369,7 @@ void ModPage::updateUi() text += "
" + tr(" by ") + authorStrs.join(", "); } - - if(current.extraDataLoaded) { + if (current.extraDataLoaded) { if (!current.extraData.donate.isEmpty()) { text += "

" + tr("Donate information: "); auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index ae3d7e77e..c9ccbaf20 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -82,6 +82,7 @@ class ModPage : public QWidget, public BasePage { void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(QString data); void onModSelected(); + virtual void openUrl(const QUrl& url); protected: Ui::ModPage* ui = nullptr; diff --git a/launcher/ui/pages/modplatform/ModPage.ui b/launcher/ui/pages/modplatform/ModPage.ui index 943f02aa2..94365aa5f 100644 --- a/launcher/ui/pages/modplatform/ModPage.ui +++ b/launcher/ui/pages/modplatform/ModPage.ui @@ -16,10 +16,10 @@ - true + false - true + false diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index fd6e32ff7..a4b7b5a14 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -39,7 +39,7 @@ #include "FlameModModel.h" #include "ui/dialogs/ModDownloadDialog.h" -FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) +FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) : ModPage(dialog, instance, new FlameAPI()) { listModel = new FlameMod::ListModel(this); @@ -53,7 +53,7 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) ui->sortByBox->addItem(tr("Sort by Author")); ui->sortByBox->addItem(tr("Sort by Downloads")); - // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's contructor... connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); @@ -78,3 +78,18 @@ bool FlameModPage::optedOut(ModPlatform::IndexedVersion& ver) const // other mod providers start loading before being selected, at least with // my Qt, so we need to implement this in every derived class... auto FlameModPage::shouldDisplay() const -> bool { return true; } + +void FlameModPage::openUrl(const QUrl& url) +{ + if (url.scheme().isEmpty()) { + QString query = url.query(QUrl::FullyDecoded); + if (query.startsWith("remoteUrl=")) { + // attempt to resolve url from warning page + query.remove(0, 10); + ModPage::openUrl({QUrl::fromPercentEncoding(query.toUtf8())}); // double decoding is necessary + return; + } + } + + ModPage::openUrl(url); +} \ No newline at end of file diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 50dedd6f4..aef9c6987 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -64,4 +64,6 @@ class FlameModPage : public ModPage { bool optedOut(ModPlatform::IndexedVersion& ver) const override; auto shouldDisplay() const -> bool override; + + void openUrl(const QUrl& url) override; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp index 62e417c8a..c531ea904 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp @@ -53,7 +53,7 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instan ui->sortByBox->addItem(tr("Sort by Last Updated")); ui->sortByBox->addItem(tr("Sort by Newest")); - // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's constructor... connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged); From b9547adce7c7222a2d3c8dc455e7619f2be7a221 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 1 Nov 2022 17:02:24 +0000 Subject: [PATCH 20/59] Add more license headers Signed-off-by: TheKodeToad --- launcher/ui/dialogs/ModDownloadDialog.cpp | 1 + launcher/ui/dialogs/ModDownloadDialog.h | 1 + launcher/ui/pages/modplatform/flame/FlameModPage.cpp | 1 + launcher/ui/pages/modplatform/flame/FlameModPage.h | 1 + 4 files changed, 4 insertions(+) diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index 38b4ffcf9..7f6f450ce 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad * * 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 diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index 125cb776a..6227b58e5 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad * * 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 diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index a4b7b5a14..faf12cea1 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad * * 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 diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index aef9c6987..da4fcdffb 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad * * 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 From d03ae34b61b6c61d0afd4a5ba0d27347c87b0726 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 1 Nov 2022 17:19:54 +0000 Subject: [PATCH 21/59] Auto-select first result Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ModPage.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 7f62fff1e..780750a82 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -261,8 +261,7 @@ void ModPage::openUrl(const QUrl& url) && url.path().startsWith("/mod/")) { prefixLength = 5; page = "modrinth"; - } - else if (APPLICATION->capabilities() & Application::SupportsFlame + } else if (APPLICATION->capabilities() & Application::SupportsFlame && url.host() == "www.curseforge.com" && url.path().toLower().startsWith("/minecraft/mc-mods/")) { prefixLength = 19; @@ -282,9 +281,14 @@ void ModPage::openUrl(const QUrl& url) // and the user isn't opening the same mod; they probably // intended to view in their web browser if (!slug.isEmpty() && !slug.contains('/') && slug != current.slug) { - ui->searchEdit->setText(slug); dialog->selectPage(page); + ui->searchEdit->setText(slug); + triggerSearch(); + connect(listModel->activeJob(), &Task::finished, [this] { + ui->packView->setCurrentIndex(listModel->index(0)); + }); + return; } } From 6c7d04043984c0c2c25d2cd646be223786defdc3 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 1 Nov 2022 18:12:27 +0000 Subject: [PATCH 22/59] Hacky tweaks Signed-off-by: TheKodeToad --- launcher/ui/dialogs/ModDownloadDialog.cpp | 13 ++++++++++--- launcher/ui/dialogs/ModDownloadDialog.h | 4 ++++ launcher/ui/pages/modplatform/ModPage.cpp | 10 ++++++---- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index 7f6f450ce..876f015a3 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -132,6 +132,8 @@ QList ModDownloadDialog::getPages() if (APPLICATION->capabilities() & Application::SupportsFlame) pages.append(FlameModPage::create(this, m_instance)); + m_selected_page = dynamic_cast(pages[0]); + return pages; } @@ -179,17 +181,22 @@ void ModDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* select return; } - auto* selected_page = dynamic_cast(selected); - if (!selected_page) { + m_selected_page = dynamic_cast(selected); + if (!m_selected_page) { qCritical() << "Page '" << selected->displayName() << "' in ModDownloadDialog is not a ModPage!"; return; } // Same effect as having a global search bar - selected_page->setSearchTerm(prev_page->getSearchTerm()); + m_selected_page->setSearchTerm(prev_page->getSearchTerm()); } bool ModDownloadDialog::selectPage(QString pageId) { return m_container->selectPage(pageId); +} + +ModPage* ModDownloadDialog::getSelectedPage() +{ + return m_selected_page; } \ No newline at end of file diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index 6227b58e5..c637a70ac 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -25,6 +25,7 @@ #include "ModDownloadTask.h" #include "minecraft/mod/ModFolderModel.h" #include "ui/pages/BasePageProvider.h" +#include "ui/pages/modplatform/ModPage.h" namespace Ui { @@ -56,6 +57,8 @@ public: bool selectPage(QString pageId); + ModPage* getSelectedPage(); + public slots: void confirm(); void accept() override; @@ -69,6 +72,7 @@ private: PageContainer * m_container = nullptr; QDialogButtonBox * m_buttons = nullptr; QVBoxLayout *m_verticalLayout = nullptr; + ModPage *m_selected_page = nullptr; QHash modTask; BaseInstance *m_instance; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 780750a82..6a53e25e5 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -282,11 +282,13 @@ void ModPage::openUrl(const QUrl& url) // intended to view in their web browser if (!slug.isEmpty() && !slug.contains('/') && slug != current.slug) { dialog->selectPage(page); - ui->searchEdit->setText(slug); - triggerSearch(); - connect(listModel->activeJob(), &Task::finished, [this] { - ui->packView->setCurrentIndex(listModel->index(0)); + ModPage* newPage = dialog->getSelectedPage(); + newPage->ui->searchEdit->setText(slug); + newPage->triggerSearch(); + + connect(newPage->listModel->activeJob(), &Task::finished, [newPage] { + newPage->ui->packView->setCurrentIndex(newPage->listModel->index(0)); }); return; From c890aa18f7b24bbb0429456d8c4a9cbb1c8d2bb1 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 1 Nov 2022 18:25:54 +0000 Subject: [PATCH 23/59] Only select correct mod Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ModPage.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 6a53e25e5..231e98f67 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -262,7 +262,7 @@ void ModPage::openUrl(const QUrl& url) prefixLength = 5; page = "modrinth"; } else if (APPLICATION->capabilities() & Application::SupportsFlame - && url.host() == "www.curseforge.com" + && (url.host() == "curseforge.com" || url.host() == "www.curseforge.com") && url.path().toLower().startsWith("/minecraft/mc-mods/")) { prefixLength = 19; page = "curseforge"; @@ -287,8 +287,15 @@ void ModPage::openUrl(const QUrl& url) newPage->ui->searchEdit->setText(slug); newPage->triggerSearch(); - connect(newPage->listModel->activeJob(), &Task::finished, [newPage] { - newPage->ui->packView->setCurrentIndex(newPage->listModel->index(0)); + connect(newPage->listModel->activeJob(), &Task::finished, [slug, newPage] { + for (int row = 0; row < newPage->listModel->rowCount({}); row++) { + QModelIndex index = newPage->listModel->index(row); + auto pack = newPage->listModel->data(index, Qt::UserRole).value(); + if (pack.slug == slug) { + newPage->ui->packView->setCurrentIndex(index); + break; + } + } }); return; From d1626d20bd4fdeeb1e9cf0f00d862fc75ddaa663 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 1 Nov 2022 18:30:11 +0000 Subject: [PATCH 24/59] Slight cleanup Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ModPage.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 231e98f67..9bb56290f 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -287,12 +287,15 @@ void ModPage::openUrl(const QUrl& url) newPage->ui->searchEdit->setText(slug); newPage->triggerSearch(); - connect(newPage->listModel->activeJob(), &Task::finished, [slug, newPage] { - for (int row = 0; row < newPage->listModel->rowCount({}); row++) { - QModelIndex index = newPage->listModel->index(row); - auto pack = newPage->listModel->data(index, Qt::UserRole).value(); + ModPlatform::ListModel* model = newPage->listModel; + QListView* view = newPage->ui->packView; + + connect(model->activeJob(), &Task::finished, [slug, model, view] { + for (int row = 0; row < model->rowCount({}); row++) { + QModelIndex index = model->index(row); + ModPlatform::IndexedPack pack = model->data(index, Qt::UserRole).value(); if (pack.slug == slug) { - newPage->ui->packView->setCurrentIndex(index); + view->setCurrentIndex(index); break; } } From 576867605dc09b1d117598908d41482e168fbc95 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 1 Nov 2022 18:40:12 +0000 Subject: [PATCH 25/59] Add another fallback Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ModPage.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 9bb56290f..f269fc72c 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -290,15 +290,18 @@ void ModPage::openUrl(const QUrl& url) ModPlatform::ListModel* model = newPage->listModel; QListView* view = newPage->ui->packView; - connect(model->activeJob(), &Task::finished, [slug, model, view] { + connect(model->activeJob(), &Task::finished, [url, slug, model, view] { for (int row = 0; row < model->rowCount({}); row++) { QModelIndex index = model->index(row); ModPlatform::IndexedPack pack = model->data(index, Qt::UserRole).value(); if (pack.slug == slug) { view->setCurrentIndex(index); - break; + return; } } + + // The final fallback. + QDesktopServices::openUrl(url); }); return; From 8dfa3393dc59a386123c84dd30287bf9f1d17faf Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 2 Nov 2022 08:43:42 +0000 Subject: [PATCH 26/59] Formatting and forward-declaration Signed-off-by: TheKodeToad --- launcher/ui/dialogs/ModDownloadDialog.cpp | 10 ++++----- launcher/ui/dialogs/ModDownloadDialog.h | 25 +++++++++++------------ launcher/ui/pages/modplatform/ModPage.cpp | 13 +++++------- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index 876f015a3..0a0e61e36 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -132,7 +132,7 @@ QList ModDownloadDialog::getPages() if (APPLICATION->capabilities() & Application::SupportsFlame) pages.append(FlameModPage::create(this, m_instance)); - m_selected_page = dynamic_cast(pages[0]); + m_selectedPage = dynamic_cast(pages[0]); return pages; } @@ -181,14 +181,14 @@ void ModDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* select return; } - m_selected_page = dynamic_cast(selected); - if (!m_selected_page) { + m_selectedPage = dynamic_cast(selected); + if (!m_selectedPage) { qCritical() << "Page '" << selected->displayName() << "' in ModDownloadDialog is not a ModPage!"; return; } // Same effect as having a global search bar - m_selected_page->setSearchTerm(prev_page->getSearchTerm()); + m_selectedPage->setSearchTerm(prev_page->getSearchTerm()); } bool ModDownloadDialog::selectPage(QString pageId) @@ -198,5 +198,5 @@ bool ModDownloadDialog::selectPage(QString pageId) ModPage* ModDownloadDialog::getSelectedPage() { - return m_selected_page; + return m_selectedPage; } \ No newline at end of file diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index c637a70ac..29bdcf82d 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -25,7 +25,6 @@ #include "ModDownloadTask.h" #include "minecraft/mod/ModFolderModel.h" #include "ui/pages/BasePageProvider.h" -#include "ui/pages/modplatform/ModPage.h" namespace Ui { @@ -34,13 +33,14 @@ class ModDownloadDialog; class PageContainer; class QDialogButtonBox; +class ModPage; class ModrinthModPage; class ModDownloadDialog final : public QDialog, public BasePageProvider { Q_OBJECT -public: + public: explicit ModDownloadDialog(const std::shared_ptr& mods, QWidget* parent, BaseInstance* instance); ~ModDownloadDialog() override = default; @@ -53,27 +53,26 @@ public: bool isModSelected(QString name) const; const QList getTasks(); - const std::shared_ptr &mods; + const std::shared_ptr& mods; bool selectPage(QString pageId); - ModPage* getSelectedPage(); -public slots: + public slots: void confirm(); void accept() override; void reject() override; -private slots: + private slots: void selectedPageChanged(BasePage* previous, BasePage* selected); -private: - Ui::ModDownloadDialog *ui = nullptr; - PageContainer * m_container = nullptr; - QDialogButtonBox * m_buttons = nullptr; - QVBoxLayout *m_verticalLayout = nullptr; - ModPage *m_selected_page = nullptr; + private: + Ui::ModDownloadDialog* ui = nullptr; + PageContainer* m_container = nullptr; + QDialogButtonBox* m_buttons = nullptr; + QVBoxLayout* m_verticalLayout = nullptr; + ModPage* m_selectedPage = nullptr; QHash modTask; - BaseInstance *m_instance; + BaseInstance* m_instance; }; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index f269fc72c..af16eaeff 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -257,17 +257,15 @@ void ModPage::openUrl(const QUrl& url) int prefixLength; const char* page; - if ((url.host() == "modrinth.com" || url.host() == "www.modrinth.com") - && url.path().startsWith("/mod/")) { + if ((url.host() == "modrinth.com" || url.host() == "www.modrinth.com") && url.path().startsWith("/mod/")) { prefixLength = 5; page = "modrinth"; - } else if (APPLICATION->capabilities() & Application::SupportsFlame - && (url.host() == "curseforge.com" || url.host() == "www.curseforge.com") - && url.path().toLower().startsWith("/minecraft/mc-mods/")) { + } else if (APPLICATION->capabilities() & Application::SupportsFlame && + (url.host() == "curseforge.com" || url.host() == "www.curseforge.com") && + url.path().toLower().startsWith("/minecraft/mc-mods/")) { prefixLength = 19; page = "curseforge"; - } - else + } else prefixLength = 0; if (prefixLength != 0) { @@ -312,7 +310,6 @@ void ModPage::openUrl(const QUrl& url) QDesktopServices::openUrl(url); } - /******** Make changes to the UI ********/ void ModPage::retranslate() From 6c45a990ef6b50e909368c3dfd41566ec5ca6986 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 2 Nov 2022 09:13:44 +0000 Subject: [PATCH 27/59] A good use of auto Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ModPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index af16eaeff..f245bfc27 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -291,7 +291,7 @@ void ModPage::openUrl(const QUrl& url) connect(model->activeJob(), &Task::finished, [url, slug, model, view] { for (int row = 0; row < model->rowCount({}); row++) { QModelIndex index = model->index(row); - ModPlatform::IndexedPack pack = model->data(index, Qt::UserRole).value(); + auto pack = model->data(index, Qt::UserRole).value(); if (pack.slug == slug) { view->setCurrentIndex(index); return; From a1ed8154f75378a25ea20781d481fb971f616dc8 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 2 Nov 2022 09:31:39 +0000 Subject: [PATCH 28/59] Another fix Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ModPage.cpp | 36 +++++++++++++---------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index f245bfc27..f347d8171 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -282,27 +282,31 @@ void ModPage::openUrl(const QUrl& url) dialog->selectPage(page); ModPage* newPage = dialog->getSelectedPage(); - newPage->ui->searchEdit->setText(slug); - newPage->triggerSearch(); + QLineEdit* searchEdit = newPage->ui->searchEdit; - ModPlatform::ListModel* model = newPage->listModel; - QListView* view = newPage->ui->packView; + if (searchEdit->text() != slug) { + searchEdit->setText(slug); + newPage->triggerSearch(); - connect(model->activeJob(), &Task::finished, [url, slug, model, view] { - for (int row = 0; row < model->rowCount({}); row++) { - QModelIndex index = model->index(row); - auto pack = model->data(index, Qt::UserRole).value(); - if (pack.slug == slug) { - view->setCurrentIndex(index); - return; + ModPlatform::ListModel* model = newPage->listModel; + QListView* view = newPage->ui->packView; + + connect(model->activeJob(), &Task::finished, [url, slug, model, view] { + for (int row = 0; row < model->rowCount({}); row++) { + QModelIndex index = model->index(row); + auto pack = model->data(index, Qt::UserRole).value(); + if (pack.slug == slug) { + view->setCurrentIndex(index); + return; + } } - } - // The final fallback. - QDesktopServices::openUrl(url); - }); + // The final fallback. + QDesktopServices::openUrl(url); + }); - return; + return; + } } } From a29d88c31305deda4136df9ad5046aed60a91afd Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 2 Nov 2022 09:59:52 +0000 Subject: [PATCH 29/59] Even more fixes Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ModPage.cpp | 54 +++++++++++++---------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index f347d8171..6ec283fca 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -254,19 +254,20 @@ void ModPage::openUrl(const QUrl& url) } // detect mod URLs and search instead - int prefixLength; + int prefixLength = 0; const char* page; if ((url.host() == "modrinth.com" || url.host() == "www.modrinth.com") && url.path().startsWith("/mod/")) { prefixLength = 5; page = "modrinth"; } else if (APPLICATION->capabilities() & Application::SupportsFlame && - (url.host() == "curseforge.com" || url.host() == "www.curseforge.com") && - url.path().toLower().startsWith("/minecraft/mc-mods/")) { - prefixLength = 19; + (url.host() == "curseforge.com" || url.host().endsWith(".curseforge.com"))) { + if (url.path().toLower().startsWith("/minecraft/mc-mods/")) + prefixLength = 19; + else if (url.path().toLower().startsWith("/projects/")) + prefixLength = 10; page = "curseforge"; - } else - prefixLength = 0; + } if (prefixLength != 0) { QString slug = url.path().mid(prefixLength); @@ -282,31 +283,36 @@ void ModPage::openUrl(const QUrl& url) dialog->selectPage(page); ModPage* newPage = dialog->getSelectedPage(); + QLineEdit* searchEdit = newPage->ui->searchEdit; + ModPlatform::ListModel* model = newPage->listModel; + QListView* view = newPage->ui->packView; - if (searchEdit->text() != slug) { - searchEdit->setText(slug); - newPage->triggerSearch(); + auto jump = [url, slug, model, view] { + for (int row = 0; row < model->rowCount({}); row++) { + QModelIndex index = model->index(row); + auto pack = model->data(index, Qt::UserRole).value(); - ModPlatform::ListModel* model = newPage->listModel; - QListView* view = newPage->ui->packView; - - connect(model->activeJob(), &Task::finished, [url, slug, model, view] { - for (int row = 0; row < model->rowCount({}); row++) { - QModelIndex index = model->index(row); - auto pack = model->data(index, Qt::UserRole).value(); - if (pack.slug == slug) { - view->setCurrentIndex(index); - return; - } + if (pack.slug == slug) { + view->setCurrentIndex(index); + return; } + } - // The final fallback. - QDesktopServices::openUrl(url); - }); + // The final fallback. + QDesktopServices::openUrl(url); + }; - return; + searchEdit->setText(slug); + newPage->triggerSearch(); + + if (!model->activeJob()) + jump(); + else { + connect(model->activeJob(), &Task::finished, jump); } + + return; } } From cb796dbdfbe2099bc77911c01c8633d9f28aedac Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 2 Nov 2022 16:38:32 +0000 Subject: [PATCH 30/59] Remove unnecessary block Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ModPage.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 6ec283fca..ec6f488fb 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -308,9 +308,8 @@ void ModPage::openUrl(const QUrl& url) if (!model->activeJob()) jump(); - else { + else connect(model->activeJob(), &Task::finished, jump); - } return; } From 3d11d044d2849c098187de4e973a8787538f4496 Mon Sep 17 00:00:00 2001 From: leo78913 Date: Tue, 25 Oct 2022 19:22:11 -0300 Subject: [PATCH 31/59] add an option to lock the toolbars Signed-off-by: leo78913 --- launcher/Application.cpp | 2 ++ launcher/ui/MainWindow.cpp | 32 +++++++++++++++++++++++++++++--- launcher/ui/MainWindow.h | 2 ++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 5772d7cad..9013577c4 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -614,6 +614,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // The cat m_settings->registerSetting("TheCat", false); + m_settings->registerSetting("ToolbarsLocked", false); + m_settings->registerSetting("InstSortMode", "Name"); m_settings->registerSetting("SelectedInstance", QString()); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 28eaa741b..ef056fcb1 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -262,6 +262,8 @@ public: TranslatedAction actionNoAccountsAdded; TranslatedAction actionNoDefaultAccount; + TranslatedAction actionLockToolbars; + QVector all_toolbuttons; QWidget *centralWidget = nullptr; @@ -420,6 +422,12 @@ public: actionManageAccounts->setCheckable(false); actionManageAccounts->setIcon(APPLICATION->getThemedIcon("accounts")); all_actions.append(&actionManageAccounts); + + actionLockToolbars = TranslatedAction(MainWindow); + actionLockToolbars->setObjectName(QStringLiteral("actionLockToolbars")); + actionLockToolbars.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Lock Toolbars")); + actionLockToolbars->setCheckable(true); + all_actions.append(&actionLockToolbars); } void createMainToolbar(QMainWindow *MainWindow) @@ -427,7 +435,6 @@ public: mainToolBar = TranslatedToolbar(MainWindow); mainToolBar->setVisible(menuBar->isNativeMenuBar() || !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); mainToolBar->setObjectName(QStringLiteral("mainToolBar")); - mainToolBar->setMovable(true); mainToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); mainToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); mainToolBar->setFloatable(false); @@ -524,6 +531,8 @@ public: viewMenu->addAction(actionCAT); viewMenu->addSeparator(); + viewMenu->addAction(actionLockToolbars); + menuBar->addMenu(foldersMenu); profileMenu = menuBar->addMenu(tr("&Accounts")); @@ -601,7 +610,6 @@ public: { newsToolBar = TranslatedToolbar(MainWindow); newsToolBar->setObjectName(QStringLiteral("newsToolBar")); - newsToolBar->setMovable(true); newsToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); newsToolBar->setIconSize(QSize(16, 16)); newsToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); @@ -736,7 +744,6 @@ public: instanceToolBar->setObjectName(QStringLiteral("instanceToolBar")); // disabled until we have an instance selected instanceToolBar->setEnabled(false); - instanceToolBar->setMovable(true); // Qt doesn't like vertical moving toolbars, so we have to force them... // See https://github.com/PolyMC/PolyMC/issues/493 connect(instanceToolBar, &QToolBar::orientationChanged, [=](Qt::Orientation){ instanceToolBar->setOrientation(Qt::Vertical); }); @@ -918,6 +925,14 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow connect(ui->actionCAT.operator->(), SIGNAL(toggled(bool)), SLOT(onCatToggled(bool))); setCatBackground(cat_enable); } + + // Lock toolbars + { + bool toolbarsLocked = APPLICATION->settings()->get("ToolbarsLocked").toBool(); + ui->actionLockToolbars->setChecked(toolbarsLocked); + connect(ui->actionLockToolbars.operator->(), SIGNAL(toggled(bool)), SLOT(lockToolbars(bool))); + lockToolbars(toolbarsLocked); + } // start instance when double-clicked connect(view, &InstanceView::activated, this, &MainWindow::instanceActivated); @@ -1073,8 +1088,19 @@ QMenu * MainWindow::createPopupMenu() { QMenu* filteredMenu = QMainWindow::createPopupMenu(); filteredMenu->removeAction( ui->mainToolBar->toggleViewAction() ); + + filteredMenu->addAction(ui->actionLockToolbars); + return filteredMenu; } +void MainWindow::lockToolbars(bool state) +{ + ui->mainToolBar->setMovable(!state); + ui->instanceToolBar->setMovable(!state); + ui->newsToolBar->setMovable(!state); + APPLICATION->settings()->set("ToolbarsLocked", state); +} + void MainWindow::konamiTriggered() { diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index cb8cb4aa1..f9d1f1c73 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -203,6 +203,8 @@ private slots: void globalSettingsClosed(); + void lockToolbars(bool); + #ifndef Q_OS_MAC void keyReleaseEvent(QKeyEvent *event) override; #endif From 76050880f16bef13ed152f379e72ce52042676a2 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 3 Nov 2022 16:14:35 -0300 Subject: [PATCH 32/59] fix: use unicode variant for marking '.index' hidden Signed-off-by: flow --- launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp index 4b8789189..cc4e252c1 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -36,7 +36,7 @@ LocalModUpdateTask::LocalModUpdateTask(QDir index_dir, ModPlatform::IndexedPack& } #ifdef Q_OS_WIN32 - SetFileAttributesA(index_dir.path().toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); + SetFileAttributesW(index_dir.path().toStdWString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); #endif } From 353b51f11ea406dd027096c30ec9626ee4e2c417 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 3 Nov 2022 16:41:55 -0300 Subject: [PATCH 33/59] refactor: move MMCStrings -> StringUtils General utilities can go in here >:) Signed-off-by: flow --- launcher/CMakeLists.txt | 4 ++-- launcher/JavaCommon.cpp | 2 +- launcher/MMCStrings.h | 8 -------- launcher/{MMCStrings.cpp => StringUtils.cpp} | 4 ++-- launcher/StringUtils.h | 7 +++++++ launcher/java/JavaInstall.cpp | 7 ++++--- launcher/java/JavaInstallList.cpp | 1 - launcher/java/JavaVersion.cpp | 7 ++++--- launcher/launch/LaunchTask.cpp | 1 - launcher/minecraft/MinecraftInstance.cpp | 1 - launcher/ui/dialogs/ExportInstanceDialog.cpp | 9 ++++----- .../ui/pages/modplatform/atlauncher/AtlFilterModel.cpp | 5 +++-- launcher/ui/pages/modplatform/flame/FlameModel.cpp | 1 - launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp | 5 +++-- launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp | 4 ++-- 15 files changed, 32 insertions(+), 34 deletions(-) delete mode 100644 launcher/MMCStrings.h rename launcher/{MMCStrings.cpp => StringUtils.cpp} (95%) create mode 100644 launcher/StringUtils.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 0dae47df8..577eba4b3 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -24,8 +24,8 @@ set(CORE_SOURCES NullInstance.h MMCZip.h MMCZip.cpp - MMCStrings.h - MMCStrings.cpp + StringUtils.h + StringUtils.cpp RuntimeContext.h # Basic instance manipulation tasks (derived from InstanceTask) diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index aa4d11233..52cc868a7 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.cpp @@ -36,7 +36,7 @@ #include "JavaCommon.h" #include "java/JavaUtils.h" #include "ui/dialogs/CustomMessageBox.h" -#include + #include bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget *parent) diff --git a/launcher/MMCStrings.h b/launcher/MMCStrings.h deleted file mode 100644 index 48052a00e..000000000 --- a/launcher/MMCStrings.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include - -namespace Strings -{ - int naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs); -} diff --git a/launcher/MMCStrings.cpp b/launcher/StringUtils.cpp similarity index 95% rename from launcher/MMCStrings.cpp rename to launcher/StringUtils.cpp index dc91c8d6a..5ae586426 100644 --- a/launcher/MMCStrings.cpp +++ b/launcher/StringUtils.cpp @@ -1,4 +1,4 @@ -#include "MMCStrings.h" +#include "StringUtils.h" /// TAKEN FROM Qt, because it doesn't expose it intelligently static inline QChar getNextChar(const QString &s, int location) @@ -7,7 +7,7 @@ static inline QChar getNextChar(const QString &s, int location) } /// TAKEN FROM Qt, because it doesn't expose it intelligently -int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs) +int StringUtils::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs) { for (int l1 = 0, l2 = 0; l1 <= s1.count() && l2 <= s2.count(); ++l1, ++l2) { diff --git a/launcher/StringUtils.h b/launcher/StringUtils.h new file mode 100644 index 000000000..fbe721171 --- /dev/null +++ b/launcher/StringUtils.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace StringUtils { +int naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs); +} // namespace StringUtils diff --git a/launcher/java/JavaInstall.cpp b/launcher/java/JavaInstall.cpp index 5bcf7bcbf..d5932bcb9 100644 --- a/launcher/java/JavaInstall.cpp +++ b/launcher/java/JavaInstall.cpp @@ -1,9 +1,10 @@ #include "JavaInstall.h" -#include + +#include "StringUtils.h" bool JavaInstall::operator<(const JavaInstall &rhs) { - auto archCompare = Strings::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive); + auto archCompare = StringUtils::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive); if(archCompare != 0) return archCompare < 0; if(id < rhs.id) @@ -14,7 +15,7 @@ bool JavaInstall::operator<(const JavaInstall &rhs) { return false; } - return Strings::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0; + return StringUtils::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0; } bool JavaInstall::operator==(const JavaInstall &rhs) diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp index 0249bd22e..d2bfde05a 100644 --- a/launcher/java/JavaInstallList.cpp +++ b/launcher/java/JavaInstallList.cpp @@ -41,7 +41,6 @@ #include "java/JavaInstallList.h" #include "java/JavaCheckerJob.h" #include "java/JavaUtils.h" -#include "MMCStrings.h" #include "minecraft/VersionFilterData.h" JavaInstallList::JavaInstallList(QObject *parent) : BaseVersionList(parent) diff --git a/launcher/java/JavaVersion.cpp b/launcher/java/JavaVersion.cpp index 179ccd8d9..0e4fc1d3c 100644 --- a/launcher/java/JavaVersion.cpp +++ b/launcher/java/JavaVersion.cpp @@ -1,5 +1,6 @@ #include "JavaVersion.h" -#include + +#include "StringUtils.h" #include #include @@ -98,12 +99,12 @@ bool JavaVersion::operator<(const JavaVersion &rhs) else if(thisPre && rhsPre) { // both are prereleases - use natural compare... - return Strings::naturalCompare(m_prerelease, rhs.m_prerelease, Qt::CaseSensitive) < 0; + return StringUtils::naturalCompare(m_prerelease, rhs.m_prerelease, Qt::CaseSensitive) < 0; } // neither is prerelease, so they are the same -> this cannot be less than rhs return false; } - else return Strings::naturalCompare(m_string, rhs.m_string, Qt::CaseSensitive) < 0; + else return StringUtils::naturalCompare(m_string, rhs.m_string, Qt::CaseSensitive) < 0; } bool JavaVersion::operator==(const JavaVersion &rhs) diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index 28fcc4f42..9e1794b34 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -37,7 +37,6 @@ #include "launch/LaunchTask.h" #include "MessageLevel.h" -#include "MMCStrings.h" #include "java/JavaChecker.h" #include "tasks/Task.h" #include diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 3a820951b..de22b365b 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -43,7 +43,6 @@ #include "settings/SettingsObject.h" #include "Application.h" -#include "MMCStrings.h" #include "pathmatcher/RegexpMatcher.h" #include "pathmatcher/MultiMatcher.h" #include "FileSystem.h" diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 9f32dd8e4..88552b239 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -39,13 +39,12 @@ #include #include #include -#include +#include #include #include -#include #include -#include "MMCStrings.h" +#include "StringUtils.h" #include "SeparatorPrefixTree.h" #include "Application.h" #include @@ -85,7 +84,7 @@ public: // sort and proxy model breaks the original model... if (sortColumn() == 0) { - return Strings::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), + return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0; } if (sortColumn() == 1) @@ -94,7 +93,7 @@ public: auto rightSize = rightFileInfo.size(); if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir())) { - return Strings::naturalCompare(leftFileInfo.fileName(), + return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0 ? asc diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp index c1ab166bc..0887ebeef 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp @@ -20,7 +20,8 @@ #include #include -#include + +#include "StringUtils.h" namespace Atl { @@ -86,7 +87,7 @@ bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) co return lv < rv; } else if (currentSorting == ByName) { - return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; + return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; } // Invalid sorting set, somehow... diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index 9f8605eb3..debae8c32 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -3,7 +3,6 @@ #include "Application.h" #include "ui/widgets/ProjectItem.h" -#include #include #include diff --git a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp index cbf347fcb..e2b548f2d 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp @@ -19,7 +19,8 @@ #include #include "modplatform/modpacksch/FTBPackManifest.h" -#include + +#include "StringUtils.h" namespace Ftb { @@ -81,7 +82,7 @@ bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) co return leftPack.installs < rightPack.installs; } else if (currentSorting == ByName) { - return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; + return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; } // Invalid sorting set, somehow... diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 2d135e597..6f11cc955 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -36,7 +36,7 @@ #include "ListModel.h" #include "Application.h" -#include +#include "StringUtils.h" #include #include @@ -66,7 +66,7 @@ bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) co return lv < rv; } else if(currentSorting == Sorting::ByName) { - return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; + return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; } //UHM, some inavlid value set?! From ab6c7244fc472de0bed761cf29700a96dd89e8ad Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 3 Nov 2022 16:44:23 -0300 Subject: [PATCH 34/59] refactor: move FS's toStdString to StringUtils This is so that anyone can use it :) Signed-off-by: flow --- launcher/FileSystem.cpp | 26 ++++++-------------------- launcher/StringUtils.h | 13 +++++++++++++ 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 4026d6c16..1cbb538c6 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -44,7 +44,9 @@ #include #include #include + #include "DesktopServices.h" +#include "StringUtils.h" #if defined Q_OS_WIN32 #include @@ -79,22 +81,6 @@ namespace fs = std::filesystem; namespace fs = ghc::filesystem; #endif -#if defined Q_OS_WIN32 - -std::wstring toStdString(QString s) -{ - return s.toStdWString(); -} - -#else - -std::string toStdString(QString s) -{ - return s.toStdString(); -} - -#endif - namespace FS { void ensureExists(const QDir& dir) @@ -191,7 +177,7 @@ bool copy::operator()(const QString& offset) auto dst_path = PathCombine(dst, relative_dst_path); ensureFilePathExists(dst_path); - fs::copy(toStdString(src_path), toStdString(dst_path), opt, err); + fs::copy(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), opt, err); if (err) { qWarning() << "Failed to copy files:" << QString::fromStdString(err.message()); qDebug() << "Source file:" << src_path; @@ -213,7 +199,7 @@ bool copy::operator()(const QString& offset) } // If the root src is not a directory, the previous iterator won't run. - if (!fs::is_directory(toStdString(src))) + if (!fs::is_directory(StringUtils::toStdString(src))) copy_file(src, ""); return err.value() == 0; @@ -223,7 +209,7 @@ bool deletePath(QString path) { std::error_code err; - fs::remove_all(toStdString(path), err); + fs::remove_all(StringUtils::toStdString(path), err); if (err) { qWarning() << "Failed to remove files:" << QString::fromStdString(err.message()); @@ -414,7 +400,7 @@ bool overrideFolder(QString overwritten_path, QString override_path) fs::copy_options opt = copy_opts::recursive | copy_opts::overwrite_existing; // FIXME: hello traveller! Apparently std::copy does NOT overwrite existing files on GNU libstdc++ on Windows? - fs::copy(toStdString(override_path), toStdString(overwritten_path), opt, err); + fs::copy(StringUtils::toStdString(override_path), StringUtils::toStdString(overwritten_path), opt, err); if (err) { qCritical() << QString("Failed to apply override from %1 to %2").arg(override_path, overwritten_path); diff --git a/launcher/StringUtils.h b/launcher/StringUtils.h index fbe721171..d7706b0f1 100644 --- a/launcher/StringUtils.h +++ b/launcher/StringUtils.h @@ -3,5 +3,18 @@ #include namespace StringUtils { + +#if defined Q_OS_WIN32 +inline std::wstring toStdString(QString s) +{ + return s.toStdWString(); +} +#else +inline std::string toStdString(QString s) +{ + return s.toStdString(); +} +#endif + int naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs); } // namespace StringUtils From dff5fea9760383984c2e60949341ebdc07eaab5a Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 3 Nov 2022 16:59:03 -0300 Subject: [PATCH 35/59] feat: add more separation between types of std::string in StringUtils Signed-off-by: flow --- launcher/StringUtils.h | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/launcher/StringUtils.h b/launcher/StringUtils.h index d7706b0f1..1799605b3 100644 --- a/launcher/StringUtils.h +++ b/launcher/StringUtils.h @@ -5,15 +5,27 @@ namespace StringUtils { #if defined Q_OS_WIN32 -inline std::wstring toStdString(QString s) +using string = std::wstring; + +inline string toStdString(QString s) { return s.toStdWString(); } +inline QString fromStdString(string s) +{ + return QString::fromStdWString(s); +} #else -inline std::string toStdString(QString s) +using string = std::string; + +inline string toStdString(QString s) { return s.toStdString(); } +inline QString fromStdString(string s) +{ + return QString::fromStdString(s); +} #endif int naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs); From d35c2db41e4a1cd9364c2160adb16ddcc4928dce Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 3 Nov 2022 16:59:50 -0300 Subject: [PATCH 36/59] fix: separate types of std::string in Packwiz Signed-off-by: flow --- launcher/modplatform/packwiz/Packwiz.cpp | 26 +++++++++++++++--------- launcher/modplatform/packwiz/Packwiz.h | 4 ---- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index b1fe963e2..0ed29311b 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -22,10 +22,14 @@ #include #include -#include +#include "FileSystem.h" +#include "StringUtils.h" + #include "minecraft/mod/Mod.h" #include "modplatform/ModIndex.h" +#include + namespace Packwiz { auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString @@ -63,22 +67,22 @@ static inline auto indexFileName(QString const& mod_slug) -> QString static ModPlatform::ProviderCapabilities ProviderCaps; // Helper functions for extracting data from the TOML file -auto stringEntry(toml::table table, const std::string entry_name) -> QString +auto stringEntry(toml::table table, QString entry_name) -> QString { - auto node = table[entry_name]; + auto node = table[StringUtils::toStdString(entry_name)]; if (!node) { - qCritical() << QString::fromStdString("Failed to read str property '" + entry_name + "' in mod metadata."); + qCritical() << "Failed to read str property '" + entry_name + "' in mod metadata."; return {}; } - return QString::fromStdString(node.value_or("")); + return node.value_or(""); } -auto intEntry(toml::table table, const std::string entry_name) -> int +auto intEntry(toml::table table, QString entry_name) -> int { - auto node = table[entry_name]; + auto node = table[StringUtils::toStdString(entry_name)]; if (!node) { - qCritical() << QString::fromStdString("Failed to read int property '" + entry_name + "' in mod metadata."); + qCritical() << "Failed to read int property '" + entry_name + "' in mod metadata."; return {}; } @@ -145,6 +149,8 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) // they want to do! if (index_file.exists()) { index_file.remove(); + } else { + FS::ensureFilePathExists(index_file.fileName()); } if (!index_file.open(QIODevice::ReadWrite)) { @@ -228,14 +234,14 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod toml::table table; #if TOML_EXCEPTIONS try { - table = toml::parse_file(index_dir.absoluteFilePath(real_fname).toStdString()); + table = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname))); } catch (const toml::parse_error& err) { qWarning() << QString("Could not open file %1!").arg(normalized_fname); qWarning() << "Reason: " << QString(err.what()); return {}; } #else - table = toml::parse_file(index_dir.absoluteFilePath(real_fname).toStdString()); + table = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname))); if (!table) { qWarning() << QString("Could not open file %1!").arg(normalized_fname); qWarning() << "Reason: " << QString(table.error().what()); diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 3ec803771..9754e5c43 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -24,7 +24,6 @@ #include #include -struct toml_table_t; class QDir; // Mod from launcher/minecraft/mod/Mod.h @@ -34,9 +33,6 @@ namespace Packwiz { auto getRealIndexName(QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString; -auto stringEntry(toml_table_t* parent, const char* entry_name) -> QString; -auto intEntry(toml_table_t* parent, const char* entry_name) -> int; - class V1 { public: struct Mod { From 016b3448e81105b60c8c7b42cd41b760eb4b60ed Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Sat, 5 Nov 2022 13:34:02 +0100 Subject: [PATCH 37/59] chore: bump to 6.0 Signed-off-by: DioEgizio <83089242+DioEgizio@users.noreply.github.com> --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 22692dae4..0db05f98a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -126,7 +126,7 @@ set(Launcher_NEWS_OPEN_URL "https://prismlauncher.org/news" CACHE STRING "URL th set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help") ######## Set version numbers ######## -set(Launcher_VERSION_MAJOR 5) +set(Launcher_VERSION_MAJOR 6) set(Launcher_VERSION_MINOR 0) set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}") From c05f744ec211a211b4a3d0f47f0cdf3efa30fd32 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sun, 23 Oct 2022 06:49:46 -0700 Subject: [PATCH 38/59] windows file association is *hard* new macros! Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- program_info/win_install.nsi.in | 160 ++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index 0cd7ea118..070d5c7c2 100644 --- a/program_info/win_install.nsi.in +++ b/program_info/win_install.nsi.in @@ -110,6 +110,135 @@ VIAddVersionKey /LANG=${LANG_ENGLISH} "LegalCopyright" "@Launcher_Copyright@" VIAddVersionKey /LANG=${LANG_ENGLISH} "FileVersion" "@Launcher_VERSION_NAME4@" VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_VERSION_NAME4@" + +;-------------------------------- +; Shell Associate Macros + +!macro APP_SETUP DESCRIPTION ICON APP_ID APP_NAME APP_EXE COMMANDTEXT COMMAND ; VERB APP_COMPANY + ; setup APP_ID + WriteRegStr ShCtx "Software\Classes\${APP_ID}" "" `${DESCRIPTION}` + WriteRegStr ShCtx "Software\Classes\${APP_ID}\DefaultIcon" "" `${ICON}` + ; default open verb + WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell" "" "open" + WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell\open\command" "" `${COMMAND}` + + ; if you want the app to use it's own implementation of a verb + ;WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell\${VERB}" "" "${DESCRIPTION}" + ;WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell\${VERB}\command" "" `${COMMAND}` + + WriteRegStr ShCtx "Software\Classes\Applications\${APP_EXE}\shell\open\command" "" `${COMMAND}` + WriteRegStr ShCtx "Software\Classes\Applications\${APP_EXE}" "FriendlyAppName" `${APP_NAME}` ; [Optional] + ;WriteRegStr ShCtx "Software\Classes\Applications\${APP_EXE}" "ApplicationCompany" `${APP_COMPANY}` ; [Optional] + ;WriteRegNone ShCtx "Software\Classes\Applications\${APP_EXE}\SupportedTypes" ".${EXT}" ; [Optional] Only allow "Open With" with specific extension(s) on WinXP+ + + # Register "Default Programs" [Optional] + !ifdef REGISTER_DEFAULTPROGRAMS + WriteRegStr ShCtx "Software\Classes\Applications\${APP_EXE}\Capabilities" "ApplicationDescription" `${DESCRIPTION}` + WriteRegStr ShCtx "Software\RegisteredApplications" `${APP_NAME}` "Software\Classes\Applications\${APP_EXE}\Capabilities" + !endif + +!macroend + +!macro APP_ASSOCIATE EXT APP_ID APP_EXE + ; Backup the previously associated file class + ReadRegStr $R0 ShCtx "Software\Classes\${EXT}" "" + WriteRegStr ShCtx "Software\Classes\${EXT}" "${APP_ID}_backup" "$R0" + + WriteRegStr ShCtx "Software\Classes\${EXT}" "" "${APP_ID}" + WriteRegNone ShCtx "Software\Classes\${EXT}\OpenWithList" "${APP_EXE}" ; Win2000+ + WriteRegNone ShCtx "Software\Classes\${EXT}\OpenWithProgids" "${APP_ID}" ; WinXP+ + + # Register "Default Programs" [Optional] + !ifdef REGISTER_DEFAULTPROGRAMS + WriteRegStr ShCtx "Software\Classes\Applications\${APP_EXE}\Capabilities\FileAssociations" "${EXT}" "${APP_ID}" + !endif + +!macroend + +!macro APP_UNASSOCIATE EXT APP_ID + + # Unregister file type + ClearErrors + ; restore backup + ReadRegStr $R0 ShCtx "Software\Classes\${EXT}" `${APP_ID}_backup` + WriteRegStr ShCtx "Software\Classes\${EXT}" "" "$R0" + + DeleteRegKey /IfEmpty ShCtx "Software\Classes\${APP_ID}" + ${IfNot} ${Errors} + ${AndIf} $R0 == "${APP_ID}" + DeleteRegValue ShCtx "Software\Classes\${EXT}" "" + DeleteRegKey /IfEmpty ShCtx "Software\Classes\${EXT}" + ${EndIf} + + DeleteRegValue ShCtx "Software\Classes\${EXT}\OpenWithList" "${APP_EXE}" + DeleteRegKey /IfEmpty ShCtx "Software\Classes\${EXT}\OpenWithList" + DeleteRegValue ShCtx "Software\Classes\${EXT}\OpenWithProgids" "${APP_ID}" + DeleteRegKey /IfEmpty ShCtx "Software\Classes\${EXT}\OpenWithProgids" + DeleteRegKey /IfEmpty ShCtx "Software\Classes\${EXT}" + + # Attempt to clean up junk left behind by the Windows shell + DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\ApplicationAssociationToasts" "${APP_ID}_${EXT}" + DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\ApplicationAssociationToasts" "Applications\${APP_EXE}_${EXT}" + DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\${EXT}\OpenWithProgids" "${APP_ID}" + DeleteRegKey /IfEmpty HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\${EXT}\OpenWithProgids" + DeleteRegKey /IfEmpty HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\${EXT}\OpenWithList" + DeleteRegKey /IfEmpty HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\${EXT}" + ;DeleteRegKey HKCU "Software\Microsoft\Windows\Roaming\OpenWith\FileExts\.${EXT}" + ;DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\RecentDocs\.${EXT}" + +!macroend + +!macro APP_TEARDOWN APP_ID APP_NAME APP_EXE + + # Unregister file type + ClearErrors + DeleteRegKey /IfEmpty ShCtx "Software\Classes\${APP_ID}\shell" + ${IfNot} ${Errors} + DeleteRegKey ShCtx "Software\Classes\${APP_ID}\DefaultIcon" + ${EndIf} + + # Unregister "Open With" + DeleteRegKey ShCtx "Software\Classes\Applications\${APP_EXE}" + + # Unregister "Default Programs" + !ifdef REGISTER_DEFAULTPROGRAMS + DeleteRegValue ShCtx "Software\RegisteredApplications" `${APP_NAME}` + DeleteRegKey ShCtx "Software\Classes\Applications\${APP_EXE}\Capabilities" + DeleteRegKey /IfEmpty ShCtx "Software\Classes\Applications\${APP_EXE}" + !endif + + DeleteRegKey ShCtx `Software\Classes\${APP_ID}` + DeleteRegKey ShCtx "Software\Classes\Applications\${APP_EXE}" + + # Attempt to clean up junk left behind by the Windows shell + DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Search\JumplistData" "$INSTDIR\${APP_EXE}" + DeleteRegValue HKCU "Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\MuiCache" "$INSTDIR\${APP_EXE}.FriendlyAppName" + DeleteRegValue HKCU "Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\MuiCache" "$INSTDIR\${APP_EXE}.ApplicationCompany" + DeleteRegValue HKCU "Software\Microsoft\Windows\ShellNoRoam\MUICache" "$INSTDIR\${APP_EXE}" ; WinXP + DeleteRegValue HKCU "Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Compatibility Assistant\Store" "$INSTDIR\${APP_EXE}" + +!macroend + +; !defines for use with SHChangeNotify +!ifdef SHCNE_ASSOCCHANGED +!undef SHCNE_ASSOCCHANGED +!endif +!define SHCNE_ASSOCCHANGED 0x08000000 +!ifdef SHCNF_FLUSH +!undef SHCNF_FLUSH +!endif +!define SHCNF_FLUSH 0x1000 + + +# ensure this is called at the end of any section that changes shell keys +!macro NotifyShell_AssocChanged +; Using the system.dll plugin to call the SHChangeNotify Win32 API function so we +; can update the shell. + System::Call "shell32::SHChangeNotify(i,i,i,i) (${SHCNE_ASSOCCHANGED}, ${SHCNF_FLUSH}, 0, 0)" +!macroend + + ;-------------------------------- ; The stuff to install @@ -171,6 +300,27 @@ Section /o "Desktop Shortcut" DESKTOP_SHORTCUTS SectionEnd + +!define APP_ID "@Launcher_CommonName@.App" +!define APP_EXE "@Launcher_APP_BINARY_NAME@.exe" +!define APP_ICON "$INSTDIR\${APP_EXE},0" +!define APP_DESCRIPTION "@Launcher_DisplayName@" +!define APP_NAME "@Launcher_DisplayName@" +!define APP_CMD_TEXT "Prism Launcher instance" + +;!define REGISTER_DEFAULTPROGRAMS "on" ; value doesn't matter + +Section -ShellAssoc + + !insertmacro APP_SETUP `${APP_DESCRIPTION}` `${APP_ICON}` `${APP_ID}` `${APP_CMD_TEXT}` `${APP_EXE}` `${APP_CMD_TEXT}` '$INSTDIR\${APP_EXE} "%1"' + + !insertmacro APP_ASSOCIATE ".zip" `${APP_ID}` `${APP_EXE}` + !insertmacro APP_ASSOCIATE ".mrpack" `${APP_ID}` `${APP_EXE}` + + !insertmacro NotifyShell_AssocChanged +SectionEnd + + ;-------------------------------- ; Uninstaller @@ -202,6 +352,16 @@ Section "Uninstall" SectionEnd +Section -un.ShellAssoc + + !insertmacro APP_TEARDOWN `${APP_ID}` `${APP_NAME}` `${APP_EXE}` + + !insertmacro APP_UNASSOCIATE ".zip" `${APP_ID}` + !insertmacro APP_UNASSOCIATE ".mrpack" `${APP_ID}` + + !insertmacro NotifyShell_AssocChanged +SectionEnd + ;-------------------------------- ; Extra command line parameters From 96008d3bb25399231a84af8fc863b6d9f3a69007 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sun, 23 Oct 2022 08:44:15 -0700 Subject: [PATCH 39/59] add -I import flag & don't clobber .zip assoc Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- program_info/win_install.nsi.in | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index 070d5c7c2..7bef0fafa 100644 --- a/program_info/win_install.nsi.in +++ b/program_info/win_install.nsi.in @@ -140,12 +140,14 @@ VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_VERSION_NAME4@ !macroend -!macro APP_ASSOCIATE EXT APP_ID APP_EXE +!macro APP_ASSOCIATE EXT APP_ID APP_EXE OVERWIRTE ; Backup the previously associated file class + ${If} ${OVERWIRTE} == true ReadRegStr $R0 ShCtx "Software\Classes\${EXT}" "" WriteRegStr ShCtx "Software\Classes\${EXT}" "${APP_ID}_backup" "$R0" - WriteRegStr ShCtx "Software\Classes\${EXT}" "" "${APP_ID}" + ${EndIf} + WriteRegNone ShCtx "Software\Classes\${EXT}\OpenWithList" "${APP_EXE}" ; Win2000+ WriteRegNone ShCtx "Software\Classes\${EXT}\OpenWithProgids" "${APP_ID}" ; WinXP+ @@ -161,8 +163,13 @@ VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_VERSION_NAME4@ # Unregister file type ClearErrors ; restore backup - ReadRegStr $R0 ShCtx "Software\Classes\${EXT}" `${APP_ID}_backup` - WriteRegStr ShCtx "Software\Classes\${EXT}" "" "$R0" + ReadRegStr $R1 ShCtx "Software\Classes\${EXT}" "" + ${If} $R1 == "${APP_ID}" + ReadRegStr $R0 ShCtx "Software\Classes\${EXT}" `${APP_ID}_backup` + WriteRegStr ShCtx "Software\Classes\${EXT}" "" "$R0" + ${Else} + ReadRegStr $R0 ShCtx "Software\Classes\${EXT}" "" + ${EndIf} DeleteRegKey /IfEmpty ShCtx "Software\Classes\${APP_ID}" ${IfNot} ${Errors} @@ -308,14 +315,14 @@ SectionEnd !define APP_NAME "@Launcher_DisplayName@" !define APP_CMD_TEXT "Prism Launcher instance" -;!define REGISTER_DEFAULTPROGRAMS "on" ; value doesn't matter +!define REGISTER_DEFAULTPROGRAMS ; value doesn't matter Section -ShellAssoc - !insertmacro APP_SETUP `${APP_DESCRIPTION}` `${APP_ICON}` `${APP_ID}` `${APP_CMD_TEXT}` `${APP_EXE}` `${APP_CMD_TEXT}` '$INSTDIR\${APP_EXE} "%1"' + !insertmacro APP_SETUP `${APP_DESCRIPTION}` `${APP_ICON}` `${APP_ID}` `${APP_CMD_TEXT}` `${APP_EXE}` `${APP_CMD_TEXT}` '$INSTDIR\${APP_EXE} -I "%1"' - !insertmacro APP_ASSOCIATE ".zip" `${APP_ID}` `${APP_EXE}` - !insertmacro APP_ASSOCIATE ".mrpack" `${APP_ID}` `${APP_EXE}` + !insertmacro APP_ASSOCIATE ".zip" `${APP_ID}` `${APP_EXE}` false + !insertmacro APP_ASSOCIATE ".mrpack" `${APP_ID}` `${APP_EXE}` true !insertmacro NotifyShell_AssocChanged SectionEnd From 9f462dcc27a614fc9098184137dfad443958b883 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 5 Nov 2022 16:28:54 -0300 Subject: [PATCH 40/59] fix: update tomlplusplus submodule This fixes an issue with Windows TOML file parsing with non-latin characters. Signed-off-by: flow --- libraries/tomlplusplus | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/tomlplusplus b/libraries/tomlplusplus index 4b166b69f..cc741c9f5 160000 --- a/libraries/tomlplusplus +++ b/libraries/tomlplusplus @@ -1 +1 @@ -Subproject commit 4b166b69f28e70a416a1a04a98f365d2aeb90de8 +Subproject commit cc741c9f5f2a62856a2a2e9e275f61eb0591c09c From 0938f80b4148cfc1734636e15da5ddb8b9a7bdfb Mon Sep 17 00:00:00 2001 From: leo78913 Date: Sat, 5 Nov 2022 18:10:36 -0300 Subject: [PATCH 41/59] Update launcher/ui/MainWindow.cpp Co-authored-by: flow Signed-off-by: leo78913 --- launcher/ui/MainWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index ef056fcb1..7bbc1bde2 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -930,7 +930,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow { bool toolbarsLocked = APPLICATION->settings()->get("ToolbarsLocked").toBool(); ui->actionLockToolbars->setChecked(toolbarsLocked); - connect(ui->actionLockToolbars.operator->(), SIGNAL(toggled(bool)), SLOT(lockToolbars(bool))); + connect(ui->actionLockToolbars, &QAction::toggled, this, &MainWindow::lockToolbars); lockToolbars(toolbarsLocked); } // start instance when double-clicked From 369419870e6d241e4e21bae2ef243dc6a12ac7d4 Mon Sep 17 00:00:00 2001 From: Adrien <66513643+AshtakaOOf@users.noreply.github.com> Date: Sat, 5 Nov 2022 23:55:43 +0100 Subject: [PATCH 42/59] add: Spooky Rory Signed-off-by: Adrien <66513643+AshtakaOOf@users.noreply.github.com> --- launcher/resources/backgrounds/backgrounds.qrc | 2 ++ .../resources/backgrounds/rory-flat-spooky.png | Bin 0 -> 66906 bytes launcher/resources/backgrounds/rory-spooky.png | Bin 0 -> 95940 bytes 3 files changed, 2 insertions(+) create mode 100644 launcher/resources/backgrounds/rory-flat-spooky.png create mode 100644 launcher/resources/backgrounds/rory-spooky.png diff --git a/launcher/resources/backgrounds/backgrounds.qrc b/launcher/resources/backgrounds/backgrounds.qrc index 652e70846..edfa44c0e 100644 --- a/launcher/resources/backgrounds/backgrounds.qrc +++ b/launcher/resources/backgrounds/backgrounds.qrc @@ -8,8 +8,10 @@ rory.png rory-xmas.png rory-bday.png + rory-spooky.png rory-flat.png rory-flat-xmas.png rory-flat-bday.png + rory-flat-spooky.png diff --git a/launcher/resources/backgrounds/rory-flat-spooky.png b/launcher/resources/backgrounds/rory-flat-spooky.png new file mode 100644 index 0000000000000000000000000000000000000000..6360c612fc2108e300cd6243be36d4530388d78d GIT binary patch literal 66906 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%0AD-#QC14AnVB=z|z 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

5njbxIHz?DEZJV~-UdQmidv~$%Ow|?P)cv!xu54 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 NJYD@<);T3K0RTZfZl?eM literal 0 HcmV?d00001 From a0580946dcac871b74e670e354921842bcf07aa6 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 6 Nov 2022 00:04:43 +0100 Subject: [PATCH 43/59] fix: allow -ween variants for all cats Signed-off-by: Sefa Eyeoglu --- launcher/ui/MainWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index afbc505ee..8cf2bde70 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1564,7 +1564,7 @@ void MainWindow::setCatBackground(bool enabled) QString cat = APPLICATION->settings()->get("BackgroundCat").toString(); if (non_stupid_abs(now.daysTo(xmas)) <= 4) { cat += "-xmas"; - } else if (cat == "kitteh" && non_stupid_abs(now.daysTo(halloween)) <= 4) { + } else if (non_stupid_abs(now.daysTo(halloween)) <= 4) { cat += "-ween"; } else if (non_stupid_abs(now.daysTo(birthday)) <= 12) { cat += "-bday"; From 4708ce4226e7b23046b8a099dd033192e68f8fd5 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 6 Nov 2022 00:47:33 +0100 Subject: [PATCH 44/59] refactor: rename halloween cats to -spooky Signed-off-by: Sefa Eyeoglu --- launcher/resources/backgrounds/backgrounds.qrc | 2 +- .../{kitteh-ween.png => kitteh-spooky.png} | Bin launcher/ui/MainWindow.cpp | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename launcher/resources/backgrounds/{kitteh-ween.png => kitteh-spooky.png} (100%) diff --git a/launcher/resources/backgrounds/backgrounds.qrc b/launcher/resources/backgrounds/backgrounds.qrc index edfa44c0e..e55faf15e 100644 --- a/launcher/resources/backgrounds/backgrounds.qrc +++ b/launcher/resources/backgrounds/backgrounds.qrc @@ -4,7 +4,7 @@ kitteh.png kitteh-xmas.png kitteh-bday.png - kitteh-ween.png + kitteh-spooky.png rory.png rory-xmas.png rory-bday.png diff --git a/launcher/resources/backgrounds/kitteh-ween.png b/launcher/resources/backgrounds/kitteh-spooky.png similarity index 100% rename from launcher/resources/backgrounds/kitteh-ween.png rename to launcher/resources/backgrounds/kitteh-spooky.png diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 8cf2bde70..f6b9888f2 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1565,7 +1565,7 @@ void MainWindow::setCatBackground(bool enabled) if (non_stupid_abs(now.daysTo(xmas)) <= 4) { cat += "-xmas"; } else if (non_stupid_abs(now.daysTo(halloween)) <= 4) { - cat += "-ween"; + cat += "-spooky"; } else if (non_stupid_abs(now.daysTo(birthday)) <= 12) { cat += "-bday"; } From d5d224d89a6d1fccacbf8ffe8d0981cec32e89fb Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 6 Nov 2022 01:05:10 +0100 Subject: [PATCH 45/59] fix: save metacache after clearing If the user closes the launcher right after clearing, it probably didn't actually clear the cache yet. Signed-off-by: Sefa Eyeoglu --- launcher/ui/MainWindow.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 28eaa741b..bbaf28272 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1899,6 +1899,7 @@ void MainWindow::on_actionReportBug_triggered() void MainWindow::on_actionClearMetadata_triggered() { APPLICATION->metacache()->evictAll(); + APPLICATION->metacache()->SaveNow(); } void MainWindow::on_actionOpenWiki_triggered() From 38e1d44dbb60aa6180a29c4a747d73395d3ed79b Mon Sep 17 00:00:00 2001 From: Adrien <66513643+AshtakaOOf@users.noreply.github.com> Date: Sun, 6 Nov 2022 03:42:51 +0100 Subject: [PATCH 46/59] Update README.md Add copr Signed-off-by: Adrien <66513643+AshtakaOOf@users.noreply.github.com> --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8ebc4fb44..611f8566e 100644 --- a/README.md +++ b/README.md @@ -27,16 +27,17 @@ There are development builds available [here](https://github.com/PrismLauncher/P Portable builds are provided for Linux, Windows, and macOS. -For Arch, Debian and Gentoo, respectively, you can use these packages to get compiled development versions: +For Arch, Debian, Fedora or Opensuse and Gentoo, respectively, you can use these packages to get compiled development versions: -[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-blue?style=flat-square)](https://aur.archlinux.org/packages/prismlauncher-qt5-git/) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-blue?style=flat-square)](https://aur.archlinux.org/packages/prismlauncher-git/) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-orange?style=flat-square)](https://mpr.makedeb.org/packages/prismlauncher-git) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-purple?style=flat-square)](https://packages.gentoo.org/packages/games-action/prismlauncher) +[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-blue?style=flat-square)](https://aur.archlinux.org/packages/prismlauncher-qt5-git/) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-blue?style=flat-square)](https://aur.archlinux.org/packages/prismlauncher-git/) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-orange?style=flat-square)](https://mpr.makedeb.org/packages/prismlauncher-git) [![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-blue?style=flat-square)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-purple?style=flat-square)](https://packages.gentoo.org/packages/games-action/prismlauncher) -## Help & Support +## Community & Support Feel free to create an issue if you need help. +We have multiple communities that can help you. #### Join our Discord server: -[![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner3)](https://discord.gg/prismlauncher) +[![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner2)](https://discord.gg/prismlauncher) #### Join our Matrix space: [![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge)](https://matrix.to/#/#prismlauncher:matrix.org) From 16e3b786fc04ffd8d510bfb2a60157648825954f Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sun, 6 Nov 2022 10:08:54 +0000 Subject: [PATCH 47/59] Implement Scrumplex's suggestions Signed-off-by: TheKodeToad --- launcher/ui/dialogs/ModDownloadDialog.cpp | 4 +- launcher/ui/dialogs/ModDownloadDialog.h | 2 +- launcher/ui/pages/modplatform/ModPage.cpp | 46 ++++++++----------- .../pages/modplatform/flame/FlameModPage.cpp | 5 +- .../ui/pages/modplatform/flame/FlameModPage.h | 2 +- 5 files changed, 27 insertions(+), 32 deletions(-) diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index 0a0e61e36..24d23ba98 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * @@ -199,4 +199,4 @@ bool ModDownloadDialog::selectPage(QString pageId) ModPage* ModDownloadDialog::getSelectedPage() { return m_selectedPage; -} \ No newline at end of file +} diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index 29bdcf82d..fcf6f4fc2 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index ec6f488fb..2f5f95bfc 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * @@ -43,6 +43,7 @@ #include #include +#include #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" @@ -254,32 +255,25 @@ void ModPage::openUrl(const QUrl& url) } // detect mod URLs and search instead - int prefixLength = 0; + static const QRegularExpression modrinth(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/mod\\/([^\\/]+)\\/?")), + curseForge(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/mc-mods\\/([^\\/]+)\\/?")), + curseForgeOld(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?")); + + const QString address = url.host() + url.path(); + QRegularExpressionMatch match; const char* page; - if ((url.host() == "modrinth.com" || url.host() == "www.modrinth.com") && url.path().startsWith("/mod/")) { - prefixLength = 5; + if ((match = modrinth.match(address)).hasMatch()) page = "modrinth"; - } else if (APPLICATION->capabilities() & Application::SupportsFlame && - (url.host() == "curseforge.com" || url.host().endsWith(".curseforge.com"))) { - if (url.path().toLower().startsWith("/minecraft/mc-mods/")) - prefixLength = 19; - else if (url.path().toLower().startsWith("/projects/")) - prefixLength = 10; + else if (APPLICATION->capabilities() & Application::SupportsFlame && + ((match = curseForge.match(address)).hasMatch() || (match = curseForgeOld.match(address)).hasMatch())) page = "curseforge"; - } - if (prefixLength != 0) { - QString slug = url.path().mid(prefixLength); + if (match.hasMatch()) { + const QString slug = match.captured(1); - // remove trailing slash(es) - while (slug.endsWith('/')) - slug.remove(slug.length() - 1, 1); - - // ensure that the path doesn't contain any further slashes, - // and the user isn't opening the same mod; they probably - // intended to view in their web browser - if (!slug.isEmpty() && !slug.contains('/') && slug != current.slug) { + // ensure the user isn't opening the same mod + if (slug != current.slug) { dialog->selectPage(page); ModPage* newPage = dialog->getSelectedPage(); @@ -290,8 +284,8 @@ void ModPage::openUrl(const QUrl& url) auto jump = [url, slug, model, view] { for (int row = 0; row < model->rowCount({}); row++) { - QModelIndex index = model->index(row); - auto pack = model->data(index, Qt::UserRole).value(); + const QModelIndex index = model->index(row); + const auto pack = model->data(index, Qt::UserRole).value(); if (pack.slug == slug) { view->setCurrentIndex(index); @@ -306,10 +300,10 @@ void ModPage::openUrl(const QUrl& url) searchEdit->setText(slug); newPage->triggerSearch(); - if (!model->activeJob()) - jump(); - else + if (model->activeJob()) connect(model->activeJob(), &Task::finished, jump); + else + jump(); return; } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index faf12cea1..bad78c97d 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * @@ -84,6 +84,7 @@ void FlameModPage::openUrl(const QUrl& url) { if (url.scheme().isEmpty()) { QString query = url.query(QUrl::FullyDecoded); + if (query.startsWith("remoteUrl=")) { // attempt to resolve url from warning page query.remove(0, 10); @@ -93,4 +94,4 @@ void FlameModPage::openUrl(const QUrl& url) } ModPage::openUrl(url); -} \ No newline at end of file +} diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index da4fcdffb..58479ab94 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * From 456999eee678cb2e59ca7d27d04bdb01eb0d7ede Mon Sep 17 00:00:00 2001 From: TheLastRar Date: Sun, 6 Nov 2022 14:23:18 +0000 Subject: [PATCH 48/59] Fix: Configuration-less config for Qt Install Signed-off-by: TheLastRar --- launcher/CMakeLists.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index f92375fb1..09a310ca1 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1066,7 +1066,7 @@ if(INSTALL_BUNDLE STREQUAL "full") # Image formats install( DIRECTORY "${QT_PLUGINS_DIR}/imageformats" - CONFIGURATIONS Debug RelWithDebInfo + CONFIGURATIONS Debug RelWithDebInfo "" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime REGEX "tga|tiff|mng" EXCLUDE @@ -1084,7 +1084,7 @@ if(INSTALL_BUNDLE STREQUAL "full") # Icon engines install( DIRECTORY "${QT_PLUGINS_DIR}/iconengines" - CONFIGURATIONS Debug RelWithDebInfo + CONFIGURATIONS Debug RelWithDebInfo "" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime REGEX "fontawesome" EXCLUDE @@ -1102,7 +1102,7 @@ if(INSTALL_BUNDLE STREQUAL "full") # Platform plugins install( DIRECTORY "${QT_PLUGINS_DIR}/platforms" - CONFIGURATIONS Debug RelWithDebInfo + CONFIGURATIONS Debug RelWithDebInfo "" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime REGEX "minimal|linuxfb|offscreen" EXCLUDE @@ -1121,7 +1121,7 @@ if(INSTALL_BUNDLE STREQUAL "full") if(EXISTS "${QT_PLUGINS_DIR}/styles") install( DIRECTORY "${QT_PLUGINS_DIR}/styles" - CONFIGURATIONS Debug RelWithDebInfo + CONFIGURATIONS Debug RelWithDebInfo "" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime ) @@ -1139,7 +1139,7 @@ if(INSTALL_BUNDLE STREQUAL "full") if(EXISTS "${QT_PLUGINS_DIR}/tls") install( DIRECTORY "${QT_PLUGINS_DIR}/tls" - CONFIGURATIONS Debug RelWithDebInfo + CONFIGURATIONS Debug RelWithDebInfo "" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime ) From 89aaac3a706f495994f87e4550be3609bb08f907 Mon Sep 17 00:00:00 2001 From: Adrien <66513643+AshtakaOOf@users.noreply.github.com> Date: Sun, 6 Nov 2022 21:56:02 +0100 Subject: [PATCH 49/59] Update README.md Signed-off-by: Adrien <66513643+AshtakaOOf@users.noreply.github.com> --- README.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 611f8566e..64fb8701d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Prism Launcher is a custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once. -This is a **fork** of the MultiMC Launcher and not endorsed by MultiMC. +This is a **fork** of the MultiMC Launcher and is not endorsed by MultiMC. ## Installation @@ -18,32 +18,31 @@ This is a **fork** of the MultiMC Launcher and not endorsed by MultiMC. Packaging status -- All downloads and instructions for Prism Launcher can be found [on our website](https://prismlauncher.org/download/). -- Last build status can be found [here](https://github.com/PrismLauncher/PrismLauncher/actions). +- All downloads and instructions for Prism Launcher can be found on our [Website](https://prismlauncher.org/download/). +- Last build status can be found in the [GitHub Actions](https://github.com/PrismLauncher/PrismLauncher/actions). ### Development Builds There are development builds available [here](https://github.com/PrismLauncher/PrismLauncher/actions). These have debug information in the binaries, so their file sizes are relatively larger. -Portable builds are provided for Linux, Windows, and macOS. +Portable builds are provided for **Linux**, **Windows**, and **macOS**. -For Arch, Debian, Fedora or Opensuse and Gentoo, respectively, you can use these packages to get compiled development versions: +For **Arch**, **Debian**, **Fedora** or **OpenSUSE** and **Gentoo**, respectively, you can use these packages for the latest development versions: -[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-blue?style=flat-square)](https://aur.archlinux.org/packages/prismlauncher-qt5-git/) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-blue?style=flat-square)](https://aur.archlinux.org/packages/prismlauncher-git/) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-orange?style=flat-square)](https://mpr.makedeb.org/packages/prismlauncher-git) [![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-blue?style=flat-square)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-purple?style=flat-square)](https://packages.gentoo.org/packages/games-action/prismlauncher) +[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-blue?style=flat-square&logo=archlinux)](https://aur.archlinux.org/packages/prismlauncher-qt5-git/) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-blue?style=flat-square&logo=archlinux)](https://aur.archlinux.org/packages/prismlauncher-git/) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-orange?style=flat-square&logo=debian)](https://mpr.makedeb.org/packages/prismlauncher-git) [![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-blue?style=flat-square&logo=fedora)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-purple?style=flat-square&logo=gentoo)](https://packages.gentoo.org/packages/games-action/prismlauncher) ## Community & Support -Feel free to create an issue if you need help. -We have multiple communities that can help you. +Feel free to create a GitHub issue if you find a bug etc. We have multiple communities that can help you. #### Join our Discord server: [![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner2)](https://discord.gg/prismlauncher) #### Join our Matrix space: -[![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge)](https://matrix.to/#/#prismlauncher:matrix.org) +[![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge&logo=matrix)](https://matrix.to/#/#prismlauncher:matrix.org) #### Join our SubReddit: -[![r/PrismLauncher](https://img.shields.io/reddit/subreddit-subscribers/prismlauncher?style=for-the-badge)](https://www.reddit.com/r/PrismLauncher/) +[![r/PrismLauncher](https://img.shields.io/reddit/subreddit-subscribers/prismlauncher?style=for-the-badge&logo=reddit)](https://www.reddit.com/r/PrismLauncher/) ## Building From 9ac6114b6379ba001785e76b5f9c10ddc2194d4a Mon Sep 17 00:00:00 2001 From: Tayou Date: Mon, 7 Nov 2022 14:33:37 +0100 Subject: [PATCH 50/59] Fix warning in main function main could according to the compiler end up not returning. of course it will always return, but I satisfied the compiler by adding a default case. Signed-off-by: Tayou --- launcher/main.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launcher/main.cpp b/launcher/main.cpp index e2116f384..df5964493 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -91,5 +91,7 @@ int main(int argc, char *argv[]) return 1; case Application::Succeeded: return 0; + default: + return -1; } } From 380e76a2e373e1b8e62561b052eeabead855c36a Mon Sep 17 00:00:00 2001 From: Adrien <66513643+AshtakaOOf@users.noreply.github.com> Date: Mon, 7 Nov 2022 17:14:16 +0100 Subject: [PATCH 51/59] Add Tumbleweed Signed-off-by: Adrien <66513643+AshtakaOOf@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 64fb8701d..07b852843 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,9 @@ There are development builds available [here](https://github.com/PrismLauncher/P Portable builds are provided for **Linux**, **Windows**, and **macOS**. -For **Arch**, **Debian**, **Fedora** or **OpenSUSE** and **Gentoo**, respectively, you can use these packages for the latest development versions: +For **Arch**, **Debian**, **Fedora**, **OpenSUSE** and **Gentoo**, respectively, you can use these packages for the latest development versions: -[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-blue?style=flat-square&logo=archlinux)](https://aur.archlinux.org/packages/prismlauncher-qt5-git/) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-blue?style=flat-square&logo=archlinux)](https://aur.archlinux.org/packages/prismlauncher-git/) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-orange?style=flat-square&logo=debian)](https://mpr.makedeb.org/packages/prismlauncher-git) [![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-blue?style=flat-square&logo=fedora)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-purple?style=flat-square&logo=gentoo)](https://packages.gentoo.org/packages/games-action/prismlauncher) +[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-blue?style=flat-square&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-qt5-git/) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-blue?style=flat-square&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-git/) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-orange?style=flat-square&logo=debian)](https://mpr.makedeb.org/packages/prismlauncher-git) [![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-blue?style=flat-square&logo=fedora&logoColor=white)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-nightly](https://img.shields.io/badge/OBS-prismlauncher--nightly-brightgreen?style=flat-square&logo=opensuse&logoColor=white)](https://build.opensuse.org/project/show/home:getchoo) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-purple?style=flat-square&logo=gentoo&logoColor=white)](https://packages.gentoo.org/packages/games-action/prismlauncher) ## Community & Support From 589d160515b7b9185bd09fc4aff5ae2c4abb4243 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 7 Nov 2022 14:04:48 -0300 Subject: [PATCH 52/59] fix: use cross-platform toStdString in FlameHasher Almost the same issue from toml++ :p Signed-off-by: flow --- launcher/modplatform/helpers/HashUtils.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/helpers/HashUtils.cpp b/launcher/modplatform/helpers/HashUtils.cpp index a7bbaba50..b18c87af9 100644 --- a/launcher/modplatform/helpers/HashUtils.cpp +++ b/launcher/modplatform/helpers/HashUtils.cpp @@ -4,6 +4,7 @@ #include #include "FileSystem.h" +#include "StringUtils.h" #include @@ -66,7 +67,7 @@ void FlameHasher::executeTask() // CF-specific auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); }; - std::ifstream file_stream(m_path.toStdString(), std::ifstream::binary); + std::ifstream file_stream(StringUtils::toStdString(m_path), std::ifstream::binary); // TODO: This is very heavy work, but apparently QtConcurrent can't use move semantics, so we can't boop this to another thread. // How do we make this non-blocking then? m_hash = QString::number(MurmurHash2(std::move(file_stream), 4 * MiB, should_filter_out)); From 245928a0647036ca94c69a0b6ea15a271944fffa Mon Sep 17 00:00:00 2001 From: Adrien <66513643+AshtakaOOf@users.noreply.github.com> Date: Mon, 7 Nov 2022 20:33:09 +0100 Subject: [PATCH 53/59] Update README.md Signed-off-by: Adrien <66513643+AshtakaOOf@users.noreply.github.com> --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 07b852843..cfb7a4af2 100644 --- a/README.md +++ b/README.md @@ -25,20 +25,20 @@ This is a **fork** of the MultiMC Launcher and is not endorsed by MultiMC. There are development builds available [here](https://github.com/PrismLauncher/PrismLauncher/actions). These have debug information in the binaries, so their file sizes are relatively larger. -Portable builds are provided for **Linux**, **Windows**, and **macOS**. +Prebuilt Development builds are provided for **Linux**, **Windows** and **macOS**. For **Arch**, **Debian**, **Fedora**, **OpenSUSE** and **Gentoo**, respectively, you can use these packages for the latest development versions: -[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-blue?style=flat-square&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-qt5-git/) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-blue?style=flat-square&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-git/) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-orange?style=flat-square&logo=debian)](https://mpr.makedeb.org/packages/prismlauncher-git) [![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-blue?style=flat-square&logo=fedora&logoColor=white)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-nightly](https://img.shields.io/badge/OBS-prismlauncher--nightly-brightgreen?style=flat-square&logo=opensuse&logoColor=white)](https://build.opensuse.org/project/show/home:getchoo) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-purple?style=flat-square&logo=gentoo&logoColor=white)](https://packages.gentoo.org/packages/games-action/prismlauncher) +[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-1793D1?style=flat-square&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-qt5-git/) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-1793D1?style=flat-square&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-git/) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-A80030?style=flat-square&logo=debian&logoColor=white)](https://mpr.makedeb.org/packages/prismlauncher-git) [![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-51A2DA?style=flat-square&logo=fedora&logoColor=white)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-nightly](https://img.shields.io/badge/OBS-prismlauncher--nightly-3AB6A9?style=flat-square&logo=opensuse&logoColor=white)](https://build.opensuse.org/project/show/home:getchoo) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-4D4270?style=flat-square&logo=gentoo&logoColor=white)](https://packages.gentoo.org/packages/games-action/prismlauncher) ## Community & Support -Feel free to create a GitHub issue if you find a bug etc. We have multiple communities that can help you. +Feel free to create a GitHub issue if you find a bug or want to suggest a new feature. We have multiple communities that can also help you. #### Join our Discord server: [![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner2)](https://discord.gg/prismlauncher) -#### Join our Matrix space: +#### Join our Matrix space (Will be opened at a later date): [![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge&logo=matrix)](https://matrix.to/#/#prismlauncher:matrix.org) #### Join our SubReddit: @@ -46,7 +46,7 @@ Feel free to create a GitHub issue if you find a bug etc. We have multiple commu ## Building -If you want to build Prism Launcher yourself, check [Build Instructions](https://prismlauncher.org/wiki/development/build-instructions/) for build instructions. +If you want to build Prism Launcher yourself, check the [Build Instructions](https://prismlauncher.org/wiki/development/build-instructions/). ## Translations @@ -97,6 +97,6 @@ Thanks to the awesome people over at [MacStadium](https://www.macstadium.com/), All launcher code is available under the GPL-3.0-only license. -![https://github.com/PrismLauncher/PrismLauncher/blob/develop/LICENSE](https://img.shields.io/github/license/PrismLauncher/PrismLauncher?style=for-the-badge) +![https://github.com/PrismLauncher/PrismLauncher/blob/develop/LICENSE](https://img.shields.io/github/license/PrismLauncher/PrismLauncher?style=for-the-badge&logo=gnu&color=C4282D) The logo and related assets are under the CC BY-SA 4.0 license. From ce2df4b36f91fd8a293504143670e046453eed19 Mon Sep 17 00:00:00 2001 From: txtsd Date: Tue, 8 Nov 2022 17:24:00 +0530 Subject: [PATCH 54/59] chore(readme): Specify openSUSE Tumbleweed Signed-off-by: txtsd --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cfb7a4af2..f8ea2e841 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ There are development builds available [here](https://github.com/PrismLauncher/P Prebuilt Development builds are provided for **Linux**, **Windows** and **macOS**. -For **Arch**, **Debian**, **Fedora**, **OpenSUSE** and **Gentoo**, respectively, you can use these packages for the latest development versions: +For **Arch**, **Debian**, **Fedora**, **OpenSUSE (Tumbleweed)** and **Gentoo**, respectively, you can use these packages for the latest development versions: [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-1793D1?style=flat-square&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-qt5-git/) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-1793D1?style=flat-square&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-git/) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-A80030?style=flat-square&logo=debian&logoColor=white)](https://mpr.makedeb.org/packages/prismlauncher-git) [![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-51A2DA?style=flat-square&logo=fedora&logoColor=white)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-nightly](https://img.shields.io/badge/OBS-prismlauncher--nightly-3AB6A9?style=flat-square&logo=opensuse&logoColor=white)](https://build.opensuse.org/project/show/home:getchoo) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-4D4270?style=flat-square&logo=gentoo&logoColor=white)](https://packages.gentoo.org/packages/games-action/prismlauncher) From 9ad6eb11a37815f7aab89163184d519a2183f4f2 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 9 Nov 2022 14:14:25 -0300 Subject: [PATCH 55/59] refactor: fix CodeQL warnings in StringUtils::naturalCompare I have no idea why this function exists, so better to just let it exist. =D Signed-off-by: flow --- launcher/StringUtils.cpp | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/launcher/StringUtils.cpp b/launcher/StringUtils.cpp index 5ae586426..0f3c3669f 100644 --- a/launcher/StringUtils.cpp +++ b/launcher/StringUtils.cpp @@ -1,26 +1,28 @@ #include "StringUtils.h" +/// If you're wondering where these came from exactly, then know you're not the only one =D + /// TAKEN FROM Qt, because it doesn't expose it intelligently -static inline QChar getNextChar(const QString &s, int location) +static inline QChar getNextChar(const QString& s, int location) { return (location < s.length()) ? s.at(location) : QChar(); } /// TAKEN FROM Qt, because it doesn't expose it intelligently -int StringUtils::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs) +int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs) { - for (int l1 = 0, l2 = 0; l1 <= s1.count() && l2 <= s2.count(); ++l1, ++l2) - { + int l1 = 0, l2 = 0; + while (l1 <= s1.count() && l2 <= s2.count()) { // skip spaces, tabs and 0's QChar c1 = getNextChar(s1, l1); while (c1.isSpace()) c1 = getNextChar(s1, ++l1); + QChar c2 = getNextChar(s2, l2); while (c2.isSpace()) c2 = getNextChar(s2, ++l2); - if (c1.isDigit() && c2.isDigit()) - { + if (c1.isDigit() && c2.isDigit()) { while (c1.digitValue() == 0) c1 = getNextChar(s1, ++l1); while (c2.digitValue() == 0) @@ -30,11 +32,8 @@ int StringUtils::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSe int lookAheadLocation2 = l2; int currentReturnValue = 0; // find the last digit, setting currentReturnValue as we go if it isn't equal - for (QChar lookAhead1 = c1, lookAhead2 = c2; - (lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length()); - lookAhead1 = getNextChar(s1, ++lookAheadLocation1), - lookAhead2 = getNextChar(s2, ++lookAheadLocation2)) - { + for (QChar lookAhead1 = c1, lookAhead2 = c2; (lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length()); + lookAhead1 = getNextChar(s1, ++lookAheadLocation1), lookAhead2 = getNextChar(s2, ++lookAheadLocation2)) { bool is1ADigit = !lookAhead1.isNull() && lookAhead1.isDigit(); bool is2ADigit = !lookAhead2.isNull() && lookAhead2.isDigit(); if (!is1ADigit && !is2ADigit) @@ -43,14 +42,10 @@ int StringUtils::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSe return -1; if (!is2ADigit) return 1; - if (currentReturnValue == 0) - { - if (lookAhead1 < lookAhead2) - { + if (currentReturnValue == 0) { + if (lookAhead1 < lookAhead2) { currentReturnValue = -1; - } - else if (lookAhead1 > lookAhead2) - { + } else if (lookAhead1 > lookAhead2) { currentReturnValue = 1; } } @@ -58,19 +53,24 @@ int StringUtils::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSe if (currentReturnValue != 0) return currentReturnValue; } - if (cs == Qt::CaseInsensitive) - { + + if (cs == Qt::CaseInsensitive) { if (!c1.isLower()) c1 = c1.toLower(); if (!c2.isLower()) c2 = c2.toLower(); } + int r = QString::localeAwareCompare(c1, c2); if (r < 0) return -1; if (r > 0) return 1; + + l1 += 1; + l2 += 1; } + // The two strings are the same (02 == 2) so fall back to the normal sort return QString::compare(s1, s2, cs); } From 99ed0b6c2ca67733a574a13cd215ec5c46c4dcfa Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 10 Nov 2022 11:14:58 +0000 Subject: [PATCH 56/59] Implement flowln's suggestions Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ModPage.cpp | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 2f5f95bfc..234f9f36f 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -40,10 +40,10 @@ #include #include +#include #include #include -#include #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" @@ -246,6 +246,10 @@ void ModPage::onModSelected() ui->packView->adjustSize(); } +static const QRegularExpression modrinth(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/mod\\/([^\\/]+)\\/?")); +static const QRegularExpression curseForge(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/mc-mods\\/([^\\/]+)\\/?")); +static const QRegularExpression curseForgeOld(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?")); + void ModPage::openUrl(const QUrl& url) { // do not allow other url schemes for security reasons @@ -255,19 +259,22 @@ void ModPage::openUrl(const QUrl& url) } // detect mod URLs and search instead - static const QRegularExpression modrinth(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/mod\\/([^\\/]+)\\/?")), - curseForge(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/mc-mods\\/([^\\/]+)\\/?")), - curseForgeOld(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?")); const QString address = url.host() + url.path(); QRegularExpressionMatch match; const char* page; - if ((match = modrinth.match(address)).hasMatch()) + match = modrinth.match(address); + if (match.hasMatch()) page = "modrinth"; - else if (APPLICATION->capabilities() & Application::SupportsFlame && - ((match = curseForge.match(address)).hasMatch() || (match = curseForgeOld.match(address)).hasMatch())) - page = "curseforge"; + else if (APPLICATION->capabilities() & Application::SupportsFlame) { + match = curseForge.match(address); + if (!match.hasMatch()) + match = curseForgeOld.match(address); + + if (match.hasMatch()) + page = "curseforge"; + } if (match.hasMatch()) { const QString slug = match.captured(1); From 2f10fa8b61dac5af5866e7ad8e72cf702f15a130 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 11 Nov 2022 05:41:32 -0700 Subject: [PATCH 57/59] add some extra debug logs for CF blocked mods Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/modplatform/flame/FlameInstanceCreationTask.cpp | 1 + launcher/modplatform/modpacksch/FTBPackInstallTask.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index f86e9335d..f0fbdc965 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -399,6 +399,7 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) message_dialog->setModal(true); if (message_dialog->exec()) { + qDebug() << "Post dialog blocked mods list: " << blocked_mods; copyBlockedMods(blocked_mods); setupDownloadJob(loop); } else { diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 70ef75716..40aee82b1 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -216,7 +216,7 @@ void PackInstallTask::onResolveModsSucceeded() m_blocked_mods); if (message_dialog->exec() == QDialog::Accepted) { - qDebug() << "Post dialog mods list: " << m_blocked_mods; + qDebug() << "Post dialog blocked mods list: " << m_blocked_mods; createInstance(); } else { From bb8ac9b99a3ffe33d2d8cf8939c54940a66cc0d6 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 11 Nov 2022 05:46:41 -0700 Subject: [PATCH 58/59] changed name of file type association Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- program_info/win_install.nsi.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index 7bef0fafa..42a9e762a 100644 --- a/program_info/win_install.nsi.in +++ b/program_info/win_install.nsi.in @@ -313,7 +313,7 @@ SectionEnd !define APP_ICON "$INSTDIR\${APP_EXE},0" !define APP_DESCRIPTION "@Launcher_DisplayName@" !define APP_NAME "@Launcher_DisplayName@" -!define APP_CMD_TEXT "Prism Launcher instance" +!define APP_CMD_TEXT "Minecraft Modpack" !define REGISTER_DEFAULTPROGRAMS ; value doesn't matter From 577069cfb4982735c038607c06245e6939d7be79 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 12 Nov 2022 19:23:57 -0300 Subject: [PATCH 59/59] fix: don't have the clear button on instance page filters This thing is otherworldly unoptimized. o.O Signed-off-by: flow --- launcher/ui/pages/instance/ExternalResourcesPage.ui | 6 +----- launcher/ui/pages/instance/VersionPage.ui | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index 76f8ec183..33a033366 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -27,11 +27,7 @@ - - - true - - + diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui index fcba5598d..14b7cd9f9 100644 --- a/launcher/ui/pages/instance/VersionPage.ui +++ b/launcher/ui/pages/instance/VersionPage.ui @@ -48,11 +48,7 @@ - - - true - - +