diff options
45 files changed, 550 insertions, 205 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3225cf1e..965d25f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,10 @@ on: description: Type of build (Debug, Release, RelWithDebInfo, MinSizeRel) type: string default: Debug + is_qt_cached: + description: Enable Qt caching or not + type: string + default: true secrets: SPARKLE_ED25519_KEY: description: Private key for signing Sparkle updates @@ -60,7 +64,7 @@ jobs: qt_ver: 6 qt_host: windows qt_arch: '' - qt_version: '6.4.0' + qt_version: '6.4.1' qt_modules: 'qt5compat qtimageformats' qt_tools: '' @@ -102,14 +106,6 @@ jobs: with: submodules: 'true' - - name: Initialize CodeQL - if: runner.os == 'Linux' && matrix.qt_ver == 6 - uses: github/codeql-action/init@v2 - with: - config-file: ./.github/codeql/codeql-config.yml - queries: security-and-quality - languages: cpp, java - - name: 'Setup MSYS2' if: runner.os == 'Windows' && matrix.msystem != '' uses: msys2/setup-msys2@v2 @@ -200,8 +196,7 @@ jobs: arch: ${{ matrix.qt_arch }} modules: ${{ matrix.qt_modules }} tools: ${{ matrix.qt_tools }} - cache: true - cache-key-prefix: ${{ matrix.qt_host }}-${{ matrix.qt_version }}-"${{ matrix.qt_modules }}"-qt_cache + cache: ${{ inputs.is_qt_cached }} - name: Prepare AppImage (Linux) if: runner.os == 'Linux' && matrix.qt_ver != 5 @@ -293,14 +288,6 @@ jobs: ctest -E "^example64|example$" --test-dir build --output-on-failure -C ${{ inputs.build_type }} ## - # CODE SCAN - ## - - - name: Perform CodeQL Analysis - if: runner.os == 'Linux' && matrix.qt_ver == 6 - uses: github/codeql-action/analyze@v2 - - ## # PACKAGE BUILDS ## @@ -510,10 +497,10 @@ jobs: echo "VERSION=$ver_short" >> $GITHUB_ENV - name: Package Snap (Linux) id: snapcraft - if: runner.os == 'Linux' && matrix.qt_ver != 5 + if: runner.os == 'Linux' && inputs.build_type == 'Debug' uses: snapcore/action-build@v1 - name: Upload Snap (Linux) - if: runner.os == 'Linux' && matrix.qt_ver != 5 + if: runner.os == 'Linux' && inputs.build_type == 'Debug' uses: actions/upload-artifact@v3 with: name: prismlauncher_${{ env.VERSION }}_amd64.snap diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..0cd1f6e4 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,35 @@ +name: "CodeQL Code Scanning" + +on: [ push, pull_request, workflow_dispatch ] + +jobs: + CodeQL: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: 'true' + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + config-file: ./.github/codeql/codeql-config.yml + queries: security-and-quality + languages: cpp, java + + - name: Install Dependencies + run: + sudo apt-get -y update + + sudo apt-get -y install ninja-build extra-cmake-modules scdoc qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 + + - name: Configure and Build + run: | + cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr -DLauncher_QT_VERSION_MAJOR=5 -G Ninja + + cmake --build build + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index 8adaa5e5..44751fbc 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -30,5 +30,6 @@ jobs: uses: ./.github/workflows/build.yml with: build_type: Debug + is_qt_cached: true secrets: SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index d415b2b1..8baa9693 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -12,6 +12,7 @@ jobs: uses: ./.github/workflows/build.yml with: build_type: Release + is_qt_cached: false secrets: SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 0db05f98..8fc0d326 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,9 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00") +# Fix aarch64 build for toml++ +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0") + # set CXXFLAGS for build targets set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}") @@ -29,7 +29,7 @@ Prebuilt Development builds are provided for **Linux**, **Windows** and **macOS* 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) +[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-1793D1?style=flat-square&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-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-qt5-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 diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 45cd9422..883f8968 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -226,7 +226,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setApplicationName(BuildConfig.LAUNCHER_NAME); setApplicationDisplayName(QString("%1 %2").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString())); - setApplicationVersion(BuildConfig.printableVersionString()); + setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT); setDesktopFileName(BuildConfig.LAUNCHER_DESKTOPFILENAME); startTime = QDateTime::currentDateTime(); diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 307240e0..a2a4f824 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -151,7 +151,7 @@ public: void copyManagedPack(BaseInstance& other); /// guess log level from a line of game log - virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level) + virtual MessageLevel::Enum guessLevel([[maybe_unused]] const QString &line, MessageLevel::Enum level) { return level; }; diff --git a/launcher/BaseVersionList.cpp b/launcher/BaseVersionList.cpp index 4ed82612..dc95e7ea 100644 --- a/launcher/BaseVersionList.cpp +++ b/launcher/BaseVersionList.cpp @@ -95,12 +95,12 @@ BaseVersionList::RoleList BaseVersionList::providesRoles() const int BaseVersionList::rowCount(const QModelIndex &parent) const { // Return count - return count(); + return parent.isValid() ? 0 : count(); } int BaseVersionList::columnCount(const QModelIndex &parent) const { - return 1; + return parent.isValid() ? 0 : 1; } QHash<int, QByteArray> BaseVersionList::roleNames() const diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 032f21f9..6aba268d 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -311,14 +311,14 @@ QModelIndex VersionProxyModel::index(int row, int column, const QModelIndex &par int VersionProxyModel::columnCount(const QModelIndex &parent) const { - return m_columns.size(); + return parent.isValid() ? 0 : m_columns.size(); } int VersionProxyModel::rowCount(const QModelIndex &parent) const { if(sourceModel()) { - return sourceModel()->rowCount(); + return sourceModel()->rowCount(parent); } return 0; } diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index 3a223d1b..01043ad2 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -242,7 +242,7 @@ Qt::DropActions IconList::supportedDropActions() const return Qt::CopyAction; } -bool IconList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +bool IconList::dropMimeData(const QMimeData *data, Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, [[maybe_unused]] const QModelIndex &parent) { if (action == Qt::IgnoreAction) return true; @@ -302,7 +302,7 @@ QVariant IconList::data(const QModelIndex &index, int role) const int IconList::rowCount(const QModelIndex &parent) const { - return icons.size(); + return parent.isValid() ? 0 : icons.size(); } void IconList::installIcons(const QStringList &iconFiles) diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp index eec1b329..242aad9f 100644 --- a/launcher/meta/Index.cpp +++ b/launcher/meta/Index.cpp @@ -58,11 +58,11 @@ QVariant Index::data(const QModelIndex &index, int role) const } int Index::rowCount(const QModelIndex &parent) const { - return m_lists.size(); + return parent.isValid() ? 0 : m_lists.size(); } int Index::columnCount(const QModelIndex &parent) const { - return 1; + return parent.isValid() ? 0 : 1; } QVariant Index::headerData(int section, Qt::Orientation orientation, int role) const { diff --git a/launcher/meta/JsonFormat.h b/launcher/meta/JsonFormat.h index 93217b7e..63128a4e 100644 --- a/launcher/meta/JsonFormat.h +++ b/launcher/meta/JsonFormat.h @@ -60,11 +60,6 @@ struct Require QString suggests; }; -inline Q_DECL_PURE_FUNCTION uint qHash(const Require &key, uint seed = 0) Q_DECL_NOTHROW -{ - return qHash(key.uid, seed); -} - using RequireSet = std::set<Require>; void parseIndex(const QJsonObject &obj, Index *ptr); diff --git a/launcher/minecraft/MojangVersionFormat.cpp b/launcher/minecraft/MojangVersionFormat.cpp index 9bbb4ada..623dcdfa 100644 --- a/launcher/minecraft/MojangVersionFormat.cpp +++ b/launcher/minecraft/MojangVersionFormat.cpp @@ -135,7 +135,7 @@ QJsonObject libDownloadInfoToJson(MojangLibraryDownloadInfo::Ptr libinfo) { out.insert("artifact", downloadInfoToJson(libinfo->artifact)); } - if(libinfo->classifiers.size()) + if(!libinfo->classifiers.isEmpty()) { QJsonObject classifiersOut; for(auto iter = libinfo->classifiers.begin(); iter != libinfo->classifiers.end(); iter++) @@ -297,7 +297,7 @@ void MojangVersionFormat::writeVersionProperties(const VersionFile* in, QJsonObj { out.insert("assetIndex", assetIndexToJson(in->mojangAssetIndex)); } - if(in->mojangDownloads.size()) + if(!in->mojangDownloads.isEmpty()) { QJsonObject downloadsOut; for(auto iter = in->mojangDownloads.begin(); iter != in->mojangDownloads.end(); iter++) @@ -306,6 +306,15 @@ void MojangVersionFormat::writeVersionProperties(const VersionFile* in, QJsonObj } out.insert("downloads", downloadsOut); } + if(!in->compatibleJavaMajors.isEmpty()) + { + QJsonArray compatibleJavaMajorsOut; + for(auto compatibleJavaMajor : in->compatibleJavaMajors) + { + compatibleJavaMajorsOut.append(compatibleJavaMajor); + } + out.insert("compatibleJavaMajors", compatibleJavaMajorsOut); + } } QJsonDocument MojangVersionFormat::versionFileToJson(const VersionFilePtr &patch) @@ -396,7 +405,7 @@ QJsonObject MojangVersionFormat::libraryToJson(Library *library) iter++; } libRoot.insert("natives", nativeList); - if (library->m_extractExcludes.size()) + if (!library->m_extractExcludes.isEmpty()) { QJsonArray excludes; QJsonObject extract; @@ -408,7 +417,7 @@ QJsonObject MojangVersionFormat::libraryToJson(Library *library) libRoot.insert("extract", extract); } } - if (library->m_rules.size()) + if (!library->m_rules.isEmpty()) { QJsonArray allRules; for (auto &rule : library->m_rules) diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index cec4a55b..280f6b26 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -63,13 +63,13 @@ LibraryPtr OneSixVersionFormat::libraryFromJson(ProblemContainer & problems, con QJsonObject OneSixVersionFormat::libraryToJson(Library *library) { QJsonObject libRoot = MojangVersionFormat::libraryToJson(library); - if (library->m_absoluteURL.size()) + if (!library->m_absoluteURL.isEmpty()) libRoot.insert("MMC-absoluteUrl", library->m_absoluteURL); - if (library->m_hint.size()) + if (!library->m_hint.isEmpty()) libRoot.insert("MMC-hint", library->m_hint); - if (library->m_filename.size()) + if (!library->m_filename.isEmpty()) libRoot.insert("MMC-filename", library->m_filename); - if (library->m_displayname.size()) + if (!library->m_displayname.isEmpty()) libRoot.insert("MMC-displayname", library->m_displayname); return libRoot; } @@ -225,11 +225,10 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc { QJsonObject agentObj = requireObject(agentVal); auto lib = libraryFromJson(*out, agentObj, filename); + QString arg = ""; - if (agentObj.contains("argument")) - { - readString(agentObj, "argument", arg); - } + readString(agentObj, "argument", arg); + AgentPtr agent(new Agent(lib, arg)); out->agents.append(agent); } @@ -332,6 +331,20 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch writeString(root, "appletClass", patch->appletClass); writeStringList(root, "+tweakers", patch->addTweakers); writeStringList(root, "+traits", patch->traits.values()); + writeStringList(root, "+jvmArgs", patch->addnJvmArguments); + if (!patch->agents.isEmpty()) + { + QJsonArray array; + for (auto value: patch->agents) + { + QJsonObject agentOut = OneSixVersionFormat::libraryToJson(value->library().get()); + if (!value->argument().isEmpty()) + agentOut.insert("argument", value->argument()); + + array.append(agentOut); + } + root.insert("+agents", array); + } if (!patch->libraries.isEmpty()) { QJsonArray array; diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 1618458f..6ce525eb 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -613,7 +613,7 @@ QVariant PackProfile::data(const QModelIndex &index, int role) const bool PackProfile::setData(const QModelIndex& index, const QVariant& value, int role) { - if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index)) + if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index.parent())) { return false; } @@ -675,12 +675,12 @@ Qt::ItemFlags PackProfile::flags(const QModelIndex &index) const int PackProfile::rowCount(const QModelIndex &parent) const { - return d->components.size(); + return parent.isValid() ? 0 : d->components.size(); } int PackProfile::columnCount(const QModelIndex &parent) const { - return NUM_COLUMNS; + return parent.isValid() ? 0 : NUM_COLUMNS; } void PackProfile::move(const int index, const MoveDirection direction) diff --git a/launcher/minecraft/Rule.h b/launcher/minecraft/Rule.h index 236f9a87..846e8e42 100644 --- a/launcher/minecraft/Rule.h +++ b/launcher/minecraft/Rule.h @@ -104,7 +104,7 @@ public: class ImplicitRule : public Rule { protected: - virtual bool applies(const Library *, const RuntimeContext & runtimeContext) + virtual bool applies(const Library *, [[maybe_unused]] const RuntimeContext & runtimeContext) { return true; } diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index aee7be35..ae29a972 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -173,7 +173,7 @@ bool WorldList::resetIcon(int row) int WorldList::columnCount(const QModelIndex &parent) const { - return 4; + return parent.isValid()? 0 : 4; } QVariant WorldList::data(const QModelIndex &index, int role) const @@ -398,8 +398,8 @@ void WorldList::installWorld(QFileInfo filename) w.install(m_dir.absolutePath()); } -bool WorldList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, - const QModelIndex &parent) +bool WorldList::dropMimeData(const QMimeData *data, Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, + [[maybe_unused]] const QModelIndex &parent) { if (action == Qt::IgnoreAction) return true; diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h index 5138e583..08294755 100644 --- a/launcher/minecraft/WorldList.h +++ b/launcher/minecraft/WorldList.h @@ -54,7 +54,7 @@ public: virtual int rowCount(const QModelIndex &parent = QModelIndex()) const { - return size(); + return parent.isValid() ? 0 : static_cast<int>(size()); }; virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index b3b57c74..9e2fd111 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -408,20 +408,20 @@ QVariant AccountList::headerData(int section, Qt::Orientation orientation, int r } } -int AccountList::rowCount(const QModelIndex &) const +int AccountList::rowCount(const QModelIndex &parent) const { // Return count - return count(); + return parent.isValid() ? 0 : count(); } -int AccountList::columnCount(const QModelIndex &) const +int AccountList::columnCount(const QModelIndex &parent) const { - return NUM_COLUMNS; + return parent.isValid() ? 0 : NUM_COLUMNS; } Qt::ItemFlags AccountList::flags(const QModelIndex &index) const { - if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + if (index.row() < 0 || index.row() >= rowCount(index.parent()) || !index.isValid()) { return Qt::NoItemFlags; } diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 66e80f4a..4ccc5d4d 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -144,7 +144,7 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in int ModFolderModel::columnCount(const QModelIndex &parent) const { - return NUM_COLUMNS; + return parent.isValid() ? 0 : NUM_COLUMNS; } Task* ModFolderModel::createUpdateTask() diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index b2356309..0310c8f6 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -426,7 +426,7 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& value, int role) { int row = index.row(); - if (row < 0 || row >= rowCount(index) || !index.isValid()) + if (row < 0 || row >= rowCount(index.parent()) || !index.isValid()) return false; if (role == Qt::CheckStateRole) diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index 25095a45..fe283b04 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -90,8 +90,8 @@ class ResourceFolderModel : public QAbstractListModel { /* Basic columns */ enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS }; - [[nodiscard]] int rowCount(const QModelIndex& = {}) const override { return size(); } - [[nodiscard]] int columnCount(const QModelIndex& = {}) const override { return NUM_COLUMNS; }; + [[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast<int>(size()); } + [[nodiscard]] int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; }; [[nodiscard]] Qt::DropActions supportedDropActions() const override; @@ -176,7 +176,7 @@ class ResourceFolderModel : public QAbstractListModel { * if the resource is complex and has more stuff to parse. */ virtual void onParseSucceeded(int ticket, QString resource_id); - virtual void onParseFailed(int ticket, QString resource_id) {} + virtual void onParseFailed(int ticket, QString resource_id) { Q_UNUSED(ticket); Q_UNUSED(resource_id); } protected: // Represents the relationship between a column's index (represented by the list index), and it's sorting key. diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index f8a6c1cf..ebac707d 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -137,7 +137,7 @@ QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orient int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const { - return NUM_COLUMNS; + return parent.isValid() ? 0 : NUM_COLUMNS; } Task* ResourcePackFolderModel::createUpdateTask() diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index a694e7b2..774f6114 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -121,7 +121,7 @@ ModDetails ReadMCModTOML(QByteArray contents) return {}; } auto modsTable = tomlModsTable0->as_table(); - if (!tomlModsTable0) { + if (!modsTable) { qWarning() << "Corrupted mods.toml? [[mods]] was not a table!"; return {}; } diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index f0fbdc96..91554b58 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -393,9 +393,10 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) qWarning() << "Blocked mods found, displaying mod list"; auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked mods found"), - tr("The following mods were blocked on third party launchers.<br/>" - "You will need to manually download them and add them to the modpack"), - blocked_mods); + tr("The following files are not available for download in third party launchers.<br/>" + "You will need to manually download them and add them to the instance."), + 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 40aee82b..4c7b7a4f 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -211,18 +211,17 @@ void PackInstallTask::onResolveModsSucceeded() qDebug() << "Blocked files found, displaying file list"; auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked files found"), - tr("The following files are not available for download in third party launchers.<br/>" - "You will need to manually download them and add them to the instance."), - m_blocked_mods); + tr("The following files are not available for download in third party launchers.<br/>" + "You will need to manually download them and add them to the instance."), + m_blocked_mods); if (message_dialog->exec() == QDialog::Accepted) { qDebug() << "Post dialog blocked mods list: " << m_blocked_mods; createInstance(); - } - else { + } else { abort(); } - + } else { createInstance(); } diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index e242dcf4..0d7ca769 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -47,7 +47,7 @@ auto MetaEntry::getFullPath() -> QString { // FIXME: make local? - return FS::PathCombine(basePath, relativePath); + return FS::PathCombine(m_basePath, m_relativePath); } HttpMetaCache::HttpMetaCache(QString path) : QObject(), m_index_file(path) @@ -99,7 +99,7 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex return staleEntry(base, resource_path); } - if (!expected_etag.isEmpty() && expected_etag != entry->etag) { + if (!expected_etag.isEmpty() && expected_etag != entry->m_etag) { // if the etag doesn't match expected, we disown the entry selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); @@ -107,17 +107,17 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex // if the file changed, check md5sum qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch(); - if (file_last_changed != entry->local_changed_timestamp) { + if (file_last_changed != entry->m_local_changed_timestamp) { QFile input(real_path); input.open(QIODevice::ReadOnly); QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5).toHex().constData(); - if (entry->md5sum != md5sum) { + if (entry->m_md5sum != md5sum) { selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } // md5sums matched... keep entry and save the new state to file - entry->local_changed_timestamp = file_last_changed; + entry->m_local_changed_timestamp = file_last_changed; SaveEventually(); } @@ -130,23 +130,23 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex } // entry passed all the checks we cared about. - entry->basePath = getBasePath(base); + entry->m_basePath = getBasePath(base); return entry; } auto HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) -> bool { - if (!m_entries.contains(stale_entry->baseId)) { - qCritical() << "Cannot add entry with unknown base: " << stale_entry->baseId.toLocal8Bit(); + if (!m_entries.contains(stale_entry->m_baseId)) { + qCritical() << "Cannot add entry with unknown base: " << stale_entry->m_baseId.toLocal8Bit(); return false; } - if (stale_entry->stale) { + if (stale_entry->m_stale) { qCritical() << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit(); return false; } - m_entries[stale_entry->baseId].entry_list[stale_entry->relativePath] = stale_entry; + m_entries[stale_entry->m_baseId].entry_list[stale_entry->m_relativePath] = stale_entry; SaveEventually(); return true; @@ -157,7 +157,7 @@ auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool if (!entry) return false; - entry->stale = true; + entry->m_stale = true; SaveEventually(); return true; } @@ -169,7 +169,7 @@ void HttpMetaCache::evictAll() qDebug() << "Evicting base" << base; for (MetaEntryPtr entry : map.entry_list) { if (!evictEntry(entry)) - qWarning() << "Unexpected missing cache entry" << entry->basePath; + qWarning() << "Unexpected missing cache entry" << entry->m_basePath; } } } @@ -177,10 +177,10 @@ void HttpMetaCache::evictAll() auto HttpMetaCache::staleEntry(QString base, QString resource_path) -> MetaEntryPtr { auto foo = new MetaEntry(); - foo->baseId = base; - foo->basePath = getBasePath(base); - foo->relativePath = resource_path; - foo->stale = true; + foo->m_baseId = base; + foo->m_basePath = getBasePath(base); + foo->m_relativePath = resource_path; + foo->m_stale = true; return MetaEntryPtr(foo); } @@ -235,23 +235,23 @@ void HttpMetaCache::Load() auto& entrymap = m_entries[base]; auto foo = new MetaEntry(); - foo->baseId = base; - foo->relativePath = Json::ensureString(element_obj, "path"); - foo->md5sum = Json::ensureString(element_obj, "md5sum"); - foo->etag = Json::ensureString(element_obj, "etag"); - foo->local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp"); - foo->remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp"); + foo->m_baseId = base; + foo->m_relativePath = Json::ensureString(element_obj, "path"); + foo->m_md5sum = Json::ensureString(element_obj, "md5sum"); + foo->m_etag = Json::ensureString(element_obj, "etag"); + foo->m_local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp"); + foo->m_remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp"); foo->makeEternal(Json::ensureBoolean(element_obj, (const QString)QStringLiteral("eternal"), false)); if (!foo->isEternal()) { - foo->current_age = Json::ensureDouble(element_obj, "current_age"); - foo->max_age = Json::ensureDouble(element_obj, "max_age"); + foo->m_current_age = Json::ensureDouble(element_obj, "current_age"); + foo->m_max_age = Json::ensureDouble(element_obj, "max_age"); } // presumed innocent until closer examination - foo->stale = false; + foo->m_stale = false; - entrymap.entry_list[foo->relativePath] = MetaEntryPtr(foo); + entrymap.entry_list[foo->m_relativePath] = MetaEntryPtr(foo); } } @@ -276,23 +276,23 @@ void HttpMetaCache::SaveNow() for (auto group : m_entries) { for (auto entry : group.entry_list) { // do not save stale entries. they are dead. - if (entry->stale) { + if (entry->m_stale) { continue; } QJsonObject entryObj; - Json::writeString(entryObj, "base", entry->baseId); - Json::writeString(entryObj, "path", entry->relativePath); - Json::writeString(entryObj, "md5sum", entry->md5sum); - Json::writeString(entryObj, "etag", entry->etag); - entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->local_changed_timestamp))); - if (!entry->remote_changed_timestamp.isEmpty()) - entryObj.insert("remote_changed_timestamp", QJsonValue(entry->remote_changed_timestamp)); + Json::writeString(entryObj, "base", entry->m_baseId); + Json::writeString(entryObj, "path", entry->m_relativePath); + Json::writeString(entryObj, "md5sum", entry->m_md5sum); + Json::writeString(entryObj, "etag", entry->m_etag); + entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->m_local_changed_timestamp))); + if (!entry->m_remote_changed_timestamp.isEmpty()) + entryObj.insert("remote_changed_timestamp", QJsonValue(entry->m_remote_changed_timestamp)); if (entry->isEternal()) { entryObj.insert("eternal", true); } else { - entryObj.insert("current_age", QJsonValue(double(entry->current_age))); - entryObj.insert("max_age", QJsonValue(double(entry->max_age))); + entryObj.insert("current_age", QJsonValue(double(entry->m_current_age))); + entryObj.insert("max_age", QJsonValue(double(entry->m_max_age))); } entriesArr.append(entryObj); } diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h index 2a07d65a..37f4b49a 100644 --- a/launcher/net/HttpMetaCache.h +++ b/launcher/net/HttpMetaCache.h @@ -49,47 +49,47 @@ class MetaEntry { MetaEntry() = default; public: - auto isStale() -> bool { return stale; } - void setStale(bool stale) { this->stale = stale; } + auto isStale() -> bool { return m_stale; } + void setStale(bool stale) { m_stale = stale; } auto getFullPath() -> QString; - auto getRemoteChangedTimestamp() -> QString { return remote_changed_timestamp; } - void setRemoteChangedTimestamp(QString remote_changed_timestamp) { this->remote_changed_timestamp = remote_changed_timestamp; } - void setLocalChangedTimestamp(qint64 timestamp) { local_changed_timestamp = timestamp; } + auto getRemoteChangedTimestamp() -> QString { return m_remote_changed_timestamp; } + void setRemoteChangedTimestamp(QString remote_changed_timestamp) { m_remote_changed_timestamp = remote_changed_timestamp; } + void setLocalChangedTimestamp(qint64 timestamp) { m_local_changed_timestamp = timestamp; } - auto getETag() -> QString { return etag; } - void setETag(QString etag) { this->etag = etag; } + auto getETag() -> QString { return m_etag; } + void setETag(QString etag) { m_etag = etag; } - auto getMD5Sum() -> QString { return md5sum; } - void setMD5Sum(QString md5sum) { this->md5sum = md5sum; } + auto getMD5Sum() -> QString { return m_md5sum; } + void setMD5Sum(QString md5sum) { m_md5sum = md5sum; } /* Whether the entry expires after some time (false) or not (true). */ - void makeEternal(bool eternal) { is_eternal = eternal; } - [[nodiscard]] bool isEternal() const { return is_eternal; } + void makeEternal(bool eternal) { m_is_eternal = eternal; } + [[nodiscard]] bool isEternal() const { return m_is_eternal; } - auto getCurrentAge() -> qint64 { return current_age; } - void setCurrentAge(qint64 age) { current_age = age; } + auto getCurrentAge() -> qint64 { return m_current_age; } + void setCurrentAge(qint64 age) { m_current_age = age; } - auto getMaximumAge() -> qint64 { return max_age; } - void setMaximumAge(qint64 age) { max_age = age; } + auto getMaximumAge() -> qint64 { return m_max_age; } + void setMaximumAge(qint64 age) { m_max_age = age; } - bool isExpired(qint64 offset) { return !is_eternal && (current_age >= max_age - offset); }; + bool isExpired(qint64 offset) { return !m_is_eternal && (m_current_age >= m_max_age - offset); }; protected: - QString baseId; - QString basePath; - QString relativePath; - QString md5sum; - QString etag; - - qint64 local_changed_timestamp = 0; - QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time - qint64 current_age = 0; - qint64 max_age = 0; - bool is_eternal = false; - - bool stale = true; + QString m_baseId; + QString m_basePath; + QString m_relativePath; + QString m_md5sum; + QString m_etag; + + qint64 m_local_changed_timestamp = 0; + QString m_remote_changed_timestamp; // QString for now, RFC 2822 encoded time + qint64 m_current_age = 0; + qint64 m_max_age = 0; + bool m_is_eternal = false; + + bool m_stale = true; }; using MetaEntryPtr = std::shared_ptr<MetaEntry>; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 6e3b95ff..929f2a85 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -49,7 +49,7 @@ #include <QKeyEvent> #include <QAction> - +#include <QActionGroup> #include <QApplication> #include <QButtonGroup> #include <QHBoxLayout> @@ -61,6 +61,7 @@ #include <QMenu> #include <QMenuBar> #include <QMessageBox> +#include <QFileDialog> #include <QInputDialog> #include <QLabel> #include <QToolButton> @@ -105,6 +106,7 @@ #include "ui/dialogs/UpdateDialog.h" #include "ui/dialogs/EditAccountDialog.h" #include "ui/dialogs/ExportInstanceDialog.h" +#include "ui/themes/ITheme.h" #include "UpdateController.h" #include "KonamiCode.h" @@ -253,6 +255,9 @@ public: QMenu * helpMenu = nullptr; TranslatedToolButton helpMenuButton; TranslatedAction actionClearMetadata; + #ifdef Q_OS_MAC + TranslatedAction actionAddToPATH; + #endif TranslatedAction actionReportBug; TranslatedAction actionDISCORD; TranslatedAction actionMATRIX; @@ -264,6 +269,8 @@ public: TranslatedAction actionLockToolbars; + TranslatedAction actionChangeTheme; + QVector<TranslatedToolButton *> all_toolbuttons; QWidget *centralWidget = nullptr; @@ -349,6 +356,14 @@ public: actionClearMetadata.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Clear cached metadata")); all_actions.append(&actionClearMetadata); + #ifdef Q_OS_MAC + actionAddToPATH = TranslatedAction(MainWindow); + actionAddToPATH->setObjectName(QStringLiteral("actionAddToPATH")); + actionAddToPATH.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Install to &PATH")); + actionAddToPATH.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Install a prismlauncher symlink to /usr/local/bin")); + all_actions.append(&actionAddToPATH); + #endif + if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { actionReportBug = TranslatedAction(MainWindow); actionReportBug->setObjectName(QStringLiteral("actionReportBug")); @@ -427,6 +442,11 @@ public: actionLockToolbars.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Lock Toolbars")); actionLockToolbars->setCheckable(true); all_actions.append(&actionLockToolbars); + + actionChangeTheme = TranslatedAction(MainWindow); + actionChangeTheme->setObjectName(QStringLiteral("actionChangeTheme")); + actionChangeTheme.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Themes")); + all_actions.append(&actionChangeTheme); } void createMainToolbar(QMainWindow *MainWindow) @@ -454,6 +474,10 @@ public: helpMenu->addAction(actionClearMetadata); + #ifdef Q_OS_MAC + helpMenu->addAction(actionAddToPATH); + #endif + if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { helpMenu->addAction(actionReportBug); } @@ -525,6 +549,8 @@ public: viewMenu = menuBar->addMenu(tr("&View")); viewMenu->setSeparatorsCollapsible(false); + viewMenu->addAction(actionChangeTheme); + viewMenu->addSeparator(); viewMenu->addAction(actionCAT); viewMenu->addSeparator(); @@ -539,6 +565,9 @@ public: helpMenu = menuBar->addMenu(tr("&Help")); helpMenu->setSeparatorsCollapsible(false); helpMenu->addAction(actionClearMetadata); + #ifdef Q_OS_MAC + helpMenu->addAction(actionAddToPATH); + #endif helpMenu->addSeparator(); helpMenu->addAction(actionAbout); helpMenu->addAction(actionOpenWiki); @@ -822,6 +851,7 @@ public: createInstanceToolbar(MainWindow); MainWindow->updateToolsMenu(); + MainWindow->updateThemeMenu(); retranslateUi(MainWindow); @@ -1271,6 +1301,38 @@ void MainWindow::updateToolsMenu() ui->actionLaunchInstance->setMenu(launchMenu); } +void MainWindow::updateThemeMenu() +{ + QMenu *themeMenu = ui->actionChangeTheme->menu(); + + if (themeMenu) { + themeMenu->clear(); + } else { + themeMenu = new QMenu(this); + } + + auto themes = APPLICATION->getValidApplicationThemes(); + + QActionGroup* themesGroup = new QActionGroup( this ); + + for (auto* theme : themes) { + QAction * themeAction = themeMenu->addAction(theme->name()); + + themeAction->setCheckable(true); + if (APPLICATION->settings()->get("ApplicationTheme").toString() == theme->id()) { + themeAction->setChecked(true); + } + themeAction->setActionGroup(themesGroup); + + connect(themeAction, &QAction::triggered, [theme]() { + APPLICATION->setApplicationTheme(theme->id(),false); + APPLICATION->settings()->set("ApplicationTheme", theme->id()); + }); + } + + ui->actionChangeTheme->setMenu(themeMenu); +} + void MainWindow::repopulateAccountsMenu() { accountMenu->clear(); @@ -1901,6 +1963,7 @@ void MainWindow::globalSettingsClosed() proxymodel->sort(0); updateMainToolBar(); updateToolsMenu(); + updateThemeMenu(); updateStatusCenter(); // This needs to be done to prevent UI elements disappearing in the event the config is changed // but Prism Launcher exits abnormally, causing the window state to never be saved: @@ -1929,6 +1992,29 @@ void MainWindow::on_actionClearMetadata_triggered() APPLICATION->metacache()->SaveNow(); } +#ifdef Q_OS_MAC +void MainWindow::on_actionAddToPATH_triggered() +{ + auto binaryPath = APPLICATION->applicationFilePath(); + auto targetPath = QString("/usr/local/bin/%1").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); + qDebug() << "Symlinking" << binaryPath << "to" << targetPath; + + QStringList args; + args << "-e"; + args << QString("do shell script \"mkdir -p /usr/local/bin && ln -sf '%1' '%2'\" with administrator privileges") + .arg(binaryPath, targetPath); + auto outcome = QProcess::execute("/usr/bin/osascript", args); + if (!outcome) { + QMessageBox::information(this, tr("Successfully added %1 to PATH").arg(BuildConfig.LAUNCHER_DISPLAYNAME), + tr("%1 was successfully added to your PATH. You can now start it by running `%2`.") + .arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.LAUNCHER_APP_BINARY_NAME)); + } else { + QMessageBox::critical(this, tr("Failed to add %1 to PATH").arg(BuildConfig.LAUNCHER_DISPLAYNAME), + tr("An error occurred while trying to add %1 to PATH").arg(BuildConfig.LAUNCHER_DISPLAYNAME)); + } +} +#endif + void MainWindow::on_actionOpenWiki_triggered() { DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg(""))); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index f9d1f1c7..0aa01ee2 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -128,6 +128,10 @@ private slots: void on_actionClearMetadata_triggered(); + #ifdef Q_OS_MAC + void on_actionAddToPATH_triggered(); + #endif + void on_actionOpenWiki_triggered(); void on_actionMoreNews_triggered(); @@ -170,6 +174,8 @@ private slots: void updateToolsMenu(); + void updateThemeMenu(); + void instanceActivated(QModelIndex); void instanceChanged(const QModelIndex ¤t, const QModelIndex &previous); diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index 2cf94250..edb4ff7d 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -6,28 +6,47 @@ #include "ui_BlockedModsDialog.h" #include <QDebug> +#include <QDragEnterEvent> +#include <QFileDialog> +#include <QFileInfo> #include <QStandardPaths> BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList<BlockedMod>& mods) - : QDialog(parent), ui(new Ui::BlockedModsDialog), mods(mods) + : QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods) { + m_hashing_task = shared_qobject_ptr<ConcurrentTask>(new ConcurrentTask(this, "MakeHashesTask", 10)); + connect(m_hashing_task.get(), &Task::finished, this, &BlockedModsDialog::hashTaskFinished); + 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); + auto downloadFolderButton = ui->buttonBox->addButton(tr("Add Download Folder"), QDialogButtonBox::ActionRole); + connect(downloadFolderButton, &QPushButton::clicked, this, &BlockedModsDialog::addDownloadFolder); - hashing_task = shared_qobject_ptr<ConcurrentTask>(new ConcurrentTask(this, "MakeHashesTask", 10)); + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged); - qDebug() << "Mods List: " << mods; + qDebug() << "[Blocked Mods Dialog] Mods List: " << mods; setupWatch(); scanPaths(); this->setWindowTitle(title); - ui->label->setText(text); - ui->labelModsFound->setText(tr("Please download the missing mods.")); + ui->labelDescription->setText(text); + ui->labelExplain->setText( + QString(tr("Your configured global mods folder and default downloads folder " + "are automatically checked for the downloaded mods and they will be copied to the instance if found.<br/>" + "Optionally, you may drag and drop the downloaded mods onto this dialog or add a folder to watch " + "if you did not download the mods to a default location.")) + .arg(APPLICATION->settings()->get("CentralModsDir").toString(), + QStandardPaths::writableLocation(QStandardPaths::DownloadLocation))); + + // force all URL handeling as external + connect(ui->textBrowserWatched, &QTextBrowser::anchorClicked, this, [](const QUrl url) { QDesktopServices::openUrl(url); }); + + setAcceptDrops(true); + update(); } @@ -36,20 +55,55 @@ BlockedModsDialog::~BlockedModsDialog() delete ui; } +void BlockedModsDialog::dragEnterEvent(QDragEnterEvent* e) +{ + if (e->mimeData()->hasUrls()) { + e->acceptProposedAction(); + } +} + +void BlockedModsDialog::dropEvent(QDropEvent* e) +{ + for (const QUrl& url : e->mimeData()->urls()) { + QString filePath = url.toLocalFile(); + qDebug() << "[Blocked Mods Dialog] Dropped file:" << filePath; + addHashTask(filePath); + + // watch for changes + QFileInfo file = QFileInfo(filePath); + QString path = file.dir().absolutePath(); + qDebug() << "[Blocked Mods Dialog] Adding watch path:" << path; + m_watcher.addPath(path); + } + scanPaths(); + update(); +} + void BlockedModsDialog::openAll() { - for (auto& mod : mods) { + for (auto& mod : m_mods) { QDesktopServices::openUrl(mod.websiteUrl); } } +void BlockedModsDialog::addDownloadFolder() +{ + QString dir = + QFileDialog::getExistingDirectory(this, tr("Select directory where you downloaded the mods"), + QStandardPaths::writableLocation(QStandardPaths::DownloadLocation), QFileDialog::ShowDirsOnly); + qDebug() << "[Blocked Mods Dialog] Adding watch path:" << dir; + m_watcher.addPath(dir); + scanPath(dir, true); + update(); +} + /// @brief update UI with current status of the blocked mod detection void BlockedModsDialog::update() { QString text; QString span; - for (auto& mod : mods) { + for (auto& mod : m_mods) { if (mod.matched) { // ✔ -> html for HEAVY CHECK MARK : ✔ span = QString(tr("<span style=\"color:green\"> ✔ Found at %1 </span>")).arg(mod.localPath); @@ -60,10 +114,17 @@ void BlockedModsDialog::update() text += QString(tr("%1: <a href='%2'>%2</a> <p>Hash: %3 %4</p> <br/>")).arg(mod.name, mod.websiteUrl, mod.hash, span); } - ui->textBrowser->setText(text); + ui->textBrowserModsListing->setText(text); + + QString watching; + for (auto& dir : m_watcher.directories()) { + watching += QString("<a href=\"%1\">%1</a><br/>").arg(dir); + } + + ui->textBrowserWatched->setText(watching); if (allModsMatched()) { - ui->labelModsFound->setText(tr("All mods found ✔")); + ui->labelModsFound->setText("<span style=\"color:green\">✔</span>" + tr("All mods found")); } else { ui->labelModsFound->setText(tr("Please download the missing mods.")); } @@ -73,8 +134,9 @@ void BlockedModsDialog::update() /// @param path the path to the changed directory void BlockedModsDialog::directoryChanged(QString path) { - qDebug() << "Directory changed: " << path; - scanPath(path); + qDebug() << "[Blocked Mods Dialog] Directory changed: " << path; + validateMatchedMods(); + scanPath(path, true); } /// @brief add the user downloads folder and the global mods folder to the filesystem watcher @@ -82,22 +144,23 @@ void BlockedModsDialog::setupWatch() { const QString downloadsFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); const QString modsFolder = APPLICATION->settings()->get("CentralModsDir").toString(); - watcher.addPath(downloadsFolder); - watcher.addPath(modsFolder); + m_watcher.addPath(downloadsFolder); + m_watcher.addPath(modsFolder); } /// @brief scan all watched folder void BlockedModsDialog::scanPaths() { - for (auto& dir : watcher.directories()) { - scanPath(dir); + for (auto& dir : m_watcher.directories()) { + scanPath(dir, false); } + runHashTask(); } /// @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, bool start_task) { QDir scan_dir(path); QDirIterator scan_it(path, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::NoIteratorFlags); @@ -108,17 +171,35 @@ void BlockedModsDialog::scanPath(QString path) continue; } - auto hash_task = Hashing::createBlockedModHasher(file, ModPlatform::Provider::FLAME, "sha1"); + addHashTask(file); + } - qDebug() << "Creating Hash task for path: " << file; + if (start_task) { + runHashTask(); + } +} - 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; }); +/// @brief add a hashing task for the file located at path, add the path to the pending set if the hasing task is already running +/// @param path the path to the local file being hashed +void BlockedModsDialog::addHashTask(QString path) +{ + qDebug() << "[Blocked Mods Dialog] adding a Hash task for" << path << "to the pending set."; + m_pending_hash_paths.insert(path); +} - hashing_task->addTask(hash_task); - } +/// @brief add a hashing task for the file located at path and connect it to check that hash against +/// our blocked mods list +/// @param path the path to the local file being hashed +void BlockedModsDialog::buildHashTask(QString path) +{ + auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::Provider::FLAME, "sha1"); + + qDebug() << "[Blocked Mods Dialog] Creating Hash task for path: " << path; + + connect(hash_task.get(), &Task::succeeded, this, [this, hash_task, path] { checkMatchHash(hash_task->getResult(), path); }); + connect(hash_task.get(), &Task::failed, this, [path] { qDebug() << "Failed to hash path: " << path; }); - hashing_task->start(); + m_hashing_task->addTask(hash_task); } /// @brief check if the computed hash for the provided path matches a blocked @@ -129,9 +210,9 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path) { bool match = false; - qDebug() << "Checking for match on hash: " << hash << "| From path:" << path; + qDebug() << "[Blocked Mods Dialog] Checking for match on hash: " << hash << "| From path:" << path; - for (auto& mod : mods) { + for (auto& mod : m_mods) { if (mod.matched) { continue; } @@ -140,7 +221,7 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path) mod.localPath = path; match = true; - qDebug() << "Hash match found:" << mod.name << hash << "| From path:" << path; + qDebug() << "[Blocked Mods Dialog] Hash match found:" << mod.name << hash << "| From path:" << path; break; } @@ -159,9 +240,9 @@ bool BlockedModsDialog::checkValidPath(QString path) QFileInfo file = QFileInfo(path); QString filename = file.fileName(); - for (auto& mod : mods) { + for (auto& mod : m_mods) { if (mod.name.compare(filename, Qt::CaseInsensitive) == 0) { - qDebug() << "Name match found:" << mod.name << "| From path:" << path; + qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path; return true; } } @@ -171,7 +252,61 @@ bool BlockedModsDialog::checkValidPath(QString path) bool BlockedModsDialog::allModsMatched() { - return std::all_of(mods.begin(), mods.end(), [](auto const& mod) { return mod.matched; }); + return std::all_of(m_mods.begin(), m_mods.end(), [](auto const& mod) { return mod.matched; }); +} + +/// @brief ensure matched file paths still exist +void BlockedModsDialog::validateMatchedMods() +{ + bool changed = false; + for (auto& mod : m_mods) { + if (mod.matched) { + QFileInfo file = QFileInfo(mod.localPath); + if (!file.exists() || !file.isFile()) { + qDebug() << "[Blocked Mods Dialog] File" << mod.localPath << "for mod" << mod.name + << "has vanshed! marking as not matched."; + mod.localPath = ""; + mod.matched = false; + changed = true; + } + } + } + if (changed) { + update(); + } +} + +/// @brief run hash task or mark a pending run if it is already runing +void BlockedModsDialog::runHashTask() +{ + if (!m_hashing_task->isRunning()) { + m_rehash_pending = false; + + if (!m_pending_hash_paths.isEmpty()) { + qDebug() << "[Blocked Mods Dialog] there are pending hash tasks, building and running tasks"; + + auto path = m_pending_hash_paths.begin(); + while (path != m_pending_hash_paths.end()) { + buildHashTask(*path); + path = m_pending_hash_paths.erase(path); + } + + m_hashing_task->start(); + } + } else { + qDebug() << "[Blocked Mods Dialog] queueing another run of the hashing task"; + qDebug() << "[Blocked Mods Dialog] pending hash tasks:" << m_pending_hash_paths; + m_rehash_pending = true; + } +} + +void BlockedModsDialog::hashTaskFinished() +{ + qDebug() << "[Blocked Mods Dialog] All hash tasks finished"; + if (m_rehash_pending) { + qDebug() << "[Blocked Mods Dialog] task finished with a rehash pending, rerunning"; + runHashTask(); + } } /// qDebug print support for the BlockedMod struct diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index 0a5c90db..dac43cba 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -31,20 +31,31 @@ public: ~BlockedModsDialog() override; +protected: + void dragEnterEvent(QDragEnterEvent *event) override; + void dropEvent(QDropEvent *event) override; private: Ui::BlockedModsDialog *ui; - QList<BlockedMod> &mods; - QFileSystemWatcher watcher; - shared_qobject_ptr<ConcurrentTask> hashing_task; + QList<BlockedMod> &m_mods; + QFileSystemWatcher m_watcher; + shared_qobject_ptr<ConcurrentTask> m_hashing_task; + QSet<QString> m_pending_hash_paths; + bool m_rehash_pending; void openAll(); + void addDownloadFolder(); void update(); void directoryChanged(QString path); void setupWatch(); void scanPaths(); - void scanPath(QString path); + void scanPath(QString path, bool start_task); + void addHashTask(QString path); + void buildHashTask(QString path); void checkMatchHash(QString hash, QString path); + void validateMatchedMods(); + void runHashTask(); + void hashTaskFinished(); bool checkValidPath(QString path); bool allModsMatched(); diff --git a/launcher/ui/dialogs/BlockedModsDialog.ui b/launcher/ui/dialogs/BlockedModsDialog.ui index 371549cf..88105178 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.ui +++ b/launcher/ui/dialogs/BlockedModsDialog.ui @@ -15,17 +15,39 @@ </property> <layout class="QVBoxLayout" name="verticalLayout"> <item> - <widget class="QLabel" name="label"> + <widget class="QLabel" name="labelDescription"> <property name="text"> <string notr="true"/> </property> <property name="textFormat"> <enum>Qt::RichText</enum> </property> + <property name="wordWrap"> + <bool>true</bool> + </property> </widget> </item> <item> - <widget class="QTextBrowser" name="textBrowser"> + <widget class="QLabel" name="labelExplain"> + <property name="text"> + <string/> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QTextBrowser" name="textBrowserModsListing"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>165</height> + </size> + </property> <property name="acceptRichText"> <bool>true</bool> </property> @@ -35,6 +57,47 @@ </widget> </item> <item> + <widget class="QLabel" name="labelWatched"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Watched Folders:</string> + </property> + </widget> + </item> + <item> + <widget class="QTextBrowser" name="textBrowserWatched"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>16</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>0</width> + <height>12</height> + </size> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="openLinks"> + <bool>false</bool> + </property> + </widget> + </item> + <item> <layout class="QHBoxLayout" name="bottomBoxH"> <item> <widget class="QLabel" name="labelModsFound"> diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 5e8bd7cc..d64bcb76 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -400,11 +400,11 @@ public: virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override { - return m_servers.size(); + return parent.isValid() ? 0 : m_servers.size(); } int columnCount(const QModelIndex & parent) const override { - return COLUMN_COUNT; + return parent.isValid() ? 0 : COLUMN_COUNT; } Server * at(int index) diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index d2636d87..36840649 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -20,8 +20,8 @@ class ListModel : public QAbstractListModel { ListModel(ModPage* parent); ~ListModel() override; - inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); }; - inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; }; + inline auto rowCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : modpacks.size(); }; + inline auto columnCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : 1; }; inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }; auto debugName() const -> QString; @@ -41,12 +41,12 @@ class ListModel : public QAbstractListModel { void requestModVersions(const ModPlatform::IndexedPack& current, QModelIndex index); virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; - virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) {}; + virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0; void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); - inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return searchState == CanPossiblyFetchMore; }; + inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return parent.isValid() ? false : searchState == CanPossiblyFetchMore; }; public slots: void searchRequestFinished(QJsonDocument& doc); diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 234f9f36..677bc4d6 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) const QString address = url.host() + url.path(); QRegularExpressionMatch match; - const char* page; + QString page; match = modrinth.match(address); if (match.hasMatch()) @@ -276,7 +276,7 @@ void ModPage::openUrl(const QUrl& url) page = "curseforge"; } - if (match.hasMatch()) { + if (!page.isNull()) { const QString slug = match.captured(1); // ensure the user isn't opening the same mod diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index ef9a9268..2ce04068 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -32,12 +32,12 @@ ListModel::~ListModel() int ListModel::rowCount(const QModelIndex &parent) const { - return modpacks.size(); + return parent.isValid() ? 0 : modpacks.size(); } int ListModel::columnCount(const QModelIndex &parent) const { - return 1; + return parent.isValid() ? 0 : 1; } QVariant ListModel::data(const QModelIndex &index, int role) const diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index 9138dcbb..cdb4532c 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -75,12 +75,12 @@ QVector<QString> AtlOptionalModListModel::getResult() { } int AtlOptionalModListModel::rowCount(const QModelIndex &parent) const { - return m_mods.size(); + return parent.isValid() ? 0 : m_mods.size(); } int AtlOptionalModListModel::columnCount(const QModelIndex &parent) const { // Enabled, Name, Description - return 3; + return parent.isValid() ? 0 : 3; } QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const { diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index debae8c3..127c3de5 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -15,12 +15,12 @@ ListModel::~ListModel() {} int ListModel::rowCount(const QModelIndex& parent) const { - return modpacks.size(); + return parent.isValid() ? 0 : modpacks.size(); } int ListModel::columnCount(const QModelIndex& parent) const { - return 1; + return parent.isValid() ? 0 : 1; } QVariant ListModel::data(const QModelIndex& index, int role) const diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp index 3a149944..ce2b2b18 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp @@ -34,12 +34,12 @@ ListModel::~ListModel() int ListModel::rowCount(const QModelIndex &parent) const { - return modpacks.size(); + return parent.isValid() ? 0 : modpacks.size(); } int ListModel::columnCount(const QModelIndex &parent) const { - return 1; + return parent.isValid() ? 0 : 1; } QVariant ListModel::data(const QModelIndex &index, int role) const diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 6f11cc95..6b1f6b89 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -125,12 +125,12 @@ QString ListModel::translatePackType(PackType type) const int ListModel::rowCount(const QModelIndex &parent) const { - return modpacks.size(); + return parent.isValid() ? 0 : modpacks.size(); } int ListModel::columnCount(const QModelIndex &parent) const { - return 1; + return parent.isValid() ? 0 : 1; } QVariant ListModel::data(const QModelIndex &index, int role) const diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 6f33e11e..3be377a1 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -55,8 +55,8 @@ class ModpackListModel : public QAbstractListModel { ModpackListModel(ModrinthPage* parent); ~ModpackListModel() override = default; - inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); }; - inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; }; + inline auto rowCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : modpacks.size(); }; + inline auto columnCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : 1; }; inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }; auto debugName() const -> QString; @@ -74,7 +74,7 @@ class ModpackListModel : public QAbstractListModel { void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); - inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return searchState == CanPossiblyFetchMore; }; + inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return parent.isValid() ? false : searchState == CanPossiblyFetchMore; }; public slots: void searchRequestFinished(QJsonDocument& doc_all); diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 742f4f2a..b2af1ac0 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -80,14 +80,14 @@ QVariant Technic::ListModel::data(const QModelIndex& index, int role) const return QVariant(); } -int Technic::ListModel::columnCount(const QModelIndex&) const +int Technic::ListModel::columnCount(const QModelIndex& parent) const { - return 1; + return parent.isValid() ? 0 : 1; } -int Technic::ListModel::rowCount(const QModelIndex&) const +int Technic::ListModel::rowCount(const QModelIndex& parent) const { - return modpacks.size(); + return parent.isValid() ? 0 : modpacks.size(); } void Technic::ListModel::searchWithTerm(const QString& term) diff --git a/libraries/tomlplusplus b/libraries/tomlplusplus -Subproject cc741c9f5f2a62856a2a2e9e275f61eb0591c09 +Subproject 0a90913abf9390b9e08ab6d3b40ac11634553f3 |