diff options
Diffstat (limited to 'launcher/ui')
24 files changed, 735 insertions, 86 deletions
diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 85b00b67..34c2ad22 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -39,6 +39,7 @@ #include "Application.h" #include "BuildConfig.h" +#include "FileSystem.h" #include "MainWindow.h" @@ -49,7 +50,7 @@ #include <QKeyEvent> #include <QAction> - +#include <QActionGroup> #include <QApplication> #include <QButtonGroup> #include <QHBoxLayout> @@ -61,6 +62,7 @@ #include <QMenu> #include <QMenuBar> #include <QMessageBox> +#include <QFileDialog> #include <QInputDialog> #include <QLabel> #include <QToolButton> @@ -105,6 +107,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" @@ -237,6 +240,7 @@ public: TranslatedAction actionLaunchInstanceOffline; TranslatedAction actionLaunchInstanceDemo; TranslatedAction actionExportInstance; + TranslatedAction actionCreateInstanceShortcut; QVector<TranslatedAction *> all_actions; LabeledToolButton *renameButton = nullptr; @@ -253,6 +257,9 @@ public: QMenu * helpMenu = nullptr; TranslatedToolButton helpMenuButton; TranslatedAction actionClearMetadata; + #ifdef Q_OS_MAC + TranslatedAction actionAddToPATH; + #endif TranslatedAction actionReportBug; TranslatedAction actionDISCORD; TranslatedAction actionMATRIX; @@ -264,6 +271,8 @@ public: TranslatedAction actionLockToolbars; + TranslatedAction actionChangeTheme; + QVector<TranslatedToolButton *> all_toolbuttons; QWidget *centralWidget = nullptr; @@ -290,7 +299,6 @@ public: actionAddInstance = TranslatedAction(MainWindow); actionAddInstance->setObjectName(QStringLiteral("actionAddInstance")); actionAddInstance->setIcon(APPLICATION->getThemedIcon("new")); - actionAddInstance->setIconVisibleInMenu(false); actionAddInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Add Instanc&e...")); actionAddInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Add a new instance.")); actionAddInstance->setShortcut(QKeySequence::New); @@ -350,6 +358,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")); @@ -428,6 +444,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) @@ -455,6 +476,10 @@ public: helpMenu->addAction(actionClearMetadata); + #ifdef Q_OS_MAC + helpMenu->addAction(actionAddToPATH); + #endif + if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { helpMenu->addAction(actionReportBug); } @@ -509,8 +534,6 @@ public: fileMenu->setSeparatorsCollapsible(false); fileMenu->addAction(actionAddInstance); fileMenu->addAction(actionLaunchInstance); - fileMenu->addAction(actionLaunchInstanceOffline); - fileMenu->addAction(actionLaunchInstanceDemo); fileMenu->addAction(actionKillInstance); fileMenu->addAction(actionCloseWindow); fileMenu->addSeparator(); @@ -528,6 +551,8 @@ public: viewMenu = menuBar->addMenu(tr("&View")); viewMenu->setSeparatorsCollapsible(false); + viewMenu->addAction(actionChangeTheme); + viewMenu->addSeparator(); viewMenu->addAction(actionCAT); viewMenu->addSeparator(); @@ -542,6 +567,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); @@ -555,10 +583,11 @@ public: helpMenu->addAction(actionDISCORD); if (!BuildConfig.SUBREDDIT_URL.isEmpty()) helpMenu->addAction(actionREDDIT); - helpMenu->addSeparator(); if(BuildConfig.UPDATER_ENABLED) + { + helpMenu->addSeparator(); helpMenu->addAction(actionCheckUpdate); - + } MainWindow->setMenuBar(menuBar); } @@ -576,6 +605,7 @@ public: actionOpenWiki->setObjectName(QStringLiteral("actionOpenWiki")); actionOpenWiki.setTextId(QT_TRANSLATE_NOOP("MainWindow", "%1 &Help")); actionOpenWiki.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the %1 wiki")); + actionOpenWiki->setIcon(APPLICATION->getThemedIcon("help")); connect(actionOpenWiki, &QAction::triggered, MainWindow, &MainWindow::on_actionOpenWiki_triggered); all_actions.append(&actionOpenWiki); @@ -583,6 +613,7 @@ public: actionNewsMenuBar->setObjectName(QStringLiteral("actionNewsMenuBar")); actionNewsMenuBar.setTextId(QT_TRANSLATE_NOOP("MainWindow", "%1 &News")); actionNewsMenuBar.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the %1 wiki")); + actionNewsMenuBar->setIcon(APPLICATION->getThemedIcon("news")); connect(actionNewsMenuBar, &QAction::triggered, MainWindow, &MainWindow::on_actionMoreNews_triggered); all_actions.append(&actionNewsMenuBar); } @@ -597,6 +628,7 @@ public: actionExportInstance->setEnabled(enabled); actionDeleteInstance->setEnabled(enabled); actionCopyInstance->setEnabled(enabled); + actionCreateInstanceShortcut->setEnabled(enabled); } void createStatusBar(QMainWindow *MainWindow) @@ -735,6 +767,15 @@ public: actionCopyInstance->setIcon(APPLICATION->getThemedIcon("copy")); all_actions.append(&actionCopyInstance); + actionCreateInstanceShortcut = TranslatedAction(MainWindow); + actionCreateInstanceShortcut->setObjectName(QStringLiteral("actionCreateInstanceShortcut")); + actionCreateInstanceShortcut.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Create Shortcut")); + actionCreateInstanceShortcut.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Creates a shortcut on your desktop to launch the selected instance.")); + //actionCreateInstanceShortcut->setShortcut(QKeySequence(tr("Ctrl+D"))); // TODO + // FIXME missing on Legacy, Flat and Flat (White) + actionCreateInstanceShortcut->setIcon(APPLICATION->getThemedIcon("shortcut")); + all_actions.append(&actionCreateInstanceShortcut); + setInstanceActionsEnabled(false); } @@ -773,6 +814,8 @@ public: instanceToolBar->addAction(actionCopyInstance); instanceToolBar->addAction(actionDeleteInstance); + instanceToolBar->addAction(actionCreateInstanceShortcut); // TODO find better position for this + QLayout * lay = instanceToolBar->layout(); for(int i = 0; i < lay->count(); i++) { @@ -822,6 +865,7 @@ public: createInstanceToolbar(MainWindow); MainWindow->updateToolsMenu(); + MainWindow->updateThemeMenu(); retranslateUi(MainWindow); @@ -1271,6 +1315,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 +1977,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 +2006,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(""))); @@ -2075,6 +2175,130 @@ void MainWindow::on_actionKillInstance_triggered() } } +void MainWindow::on_actionCreateInstanceShortcut_triggered() +{ + if (m_selectedInstance) + { + auto desktopPath = FS::getDesktopDir(); + if (desktopPath.isEmpty()) { + // TODO come up with an alternative solution (open "save file" dialog) + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!")); + return; + } + +#if defined(Q_OS_MACOS) + QString appPath = QApplication::applicationFilePath(); + if (appPath.startsWith("/private/var/")) { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); + return; + } + + if (FS::createShortcut(FS::PathCombine(desktopPath, m_selectedInstance->name()), + appPath, { "--launch", m_selectedInstance->id() }, + m_selectedInstance->name(), "")) { + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); + } + else + { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); + } +#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + QString appPath = QApplication::applicationFilePath(); + if (appPath.startsWith("/tmp/.mount_")) { + // AppImage! + appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + if (appPath.isEmpty()) + { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); + } + else if (appPath.endsWith("/")) + { + appPath.chop(1); + } + } + + auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (icon == nullptr) + { + icon = APPLICATION->icons()->icon("grass"); + } + + QString iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png"); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) + { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); + iconFile.close(); + + if (!success) + { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + + if (FS::createShortcut(FS::PathCombine(desktopPath, m_selectedInstance->name()), + appPath, { "--launch", m_selectedInstance->id() }, + m_selectedInstance->name(), iconPath)) { + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); + } + else + { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); + } +#elif defined(Q_OS_WIN) + auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (icon == nullptr) + { + icon = APPLICATION->icons()->icon("grass"); + } + + QString iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico"); + + // part of fix for weird bug involving the window icon being replaced + // dunno why it happens, but this 2-line fix seems to be enough, so w/e + auto appIcon = APPLICATION->getThemedIcon("logo"); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) + { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); + iconFile.close(); + + // restore original window icon + QGuiApplication::setWindowIcon(appIcon); + + if (!success) + { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + + if (FS::createShortcut(FS::PathCombine(desktopPath, m_selectedInstance->name()), + QApplication::applicationFilePath(), { "--launch", m_selectedInstance->id() }, + m_selectedInstance->name(), iconPath)) { + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); + } + else + { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); + } +#else + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!")); +#endif + } +} + void MainWindow::taskEnd() { QObject *sender = QObject::sender(); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index f9d1f1c7..f96f641d 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(); @@ -157,6 +161,8 @@ private slots: void on_actionEditInstance_triggered(); + void on_actionCreateInstanceShortcut_triggered(); + void taskEnd(); /** @@ -170,6 +176,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 fe87b517..edb4ff7d 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -1,28 +1,321 @@ #include "BlockedModsDialog.h" -#include "ui_BlockedModsDialog.h" -#include <QPushButton> -#include <QDialogButtonBox> #include <QDesktopServices> +#include <QDialogButtonBox> +#include <QPushButton> +#include "Application.h" +#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), 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); -BlockedModsDialog::BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, const QString &body, const QList<QUrl> &urls) : - QDialog(parent), ui(new Ui::BlockedModsDialog), urls(urls) { ui->setupUi(this); auto openAllButton = ui->buttonBox->addButton(tr("Open All"), QDialogButtonBox::ActionRole); connect(openAllButton, &QPushButton::clicked, this, &BlockedModsDialog::openAll); + auto downloadFolderButton = ui->buttonBox->addButton(tr("Add Download Folder"), QDialogButtonBox::ActionRole); + connect(downloadFolderButton, &QPushButton::clicked, this, &BlockedModsDialog::addDownloadFolder); + + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged); + + qDebug() << "[Blocked Mods Dialog] Mods List: " << mods; + + setupWatch(); + scanPaths(); + this->setWindowTitle(title); - ui->label->setText(text); - ui->textBrowser->setText(body); + 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(); } -BlockedModsDialog::~BlockedModsDialog() { +BlockedModsDialog::~BlockedModsDialog() +{ delete ui; } -void BlockedModsDialog::openAll() { - for(auto &url : urls) { - QDesktopServices::openUrl(url); +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 : 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 : m_mods) { + if (mod.matched) { + // ✔ -> html for HEAVY CHECK MARK : ✔ + span = QString(tr("<span style=\"color:green\"> ✔ Found at %1 </span>")).arg(mod.localPath); + } else { + // ✘ -> html for HEAVY BALLOT X : ✘ + span = QString(tr("<span style=\"color:red\"> ✘ Not Found </span>")); + } + text += QString(tr("%1: <a href='%2'>%2</a> <p>Hash: %3 %4</p> <br/>")).arg(mod.name, mod.websiteUrl, mod.hash, span); + } + + 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("<span style=\"color:green\">✔</span>" + tr("All mods found")); + } else { + ui->labelModsFound->setText(tr("Please download the missing mods.")); + } +} + +/// @brief Signal fired when a watched direcotry has changed +/// @param path the path to the changed directory +void BlockedModsDialog::directoryChanged(QString 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 +void BlockedModsDialog::setupWatch() +{ + const QString downloadsFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + const QString modsFolder = APPLICATION->settings()->get("CentralModsDir").toString(); + m_watcher.addPath(downloadsFolder); + m_watcher.addPath(modsFolder); +} + +/// @brief scan all watched folder +void BlockedModsDialog::scanPaths() +{ + 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, bool start_task) +{ + 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 (!checkValidPath(file)) { + continue; + } + + addHashTask(file); + } + + if (start_task) { + runHashTask(); + } +} + +/// @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); +} + +/// @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; }); + + m_hashing_task->addTask(hash_task); +} + +/// @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) +{ + bool match = false; + + qDebug() << "[Blocked Mods Dialog] Checking for match on hash: " << hash << "| From path:" << path; + + for (auto& mod : m_mods) { + if (mod.matched) { + continue; + } + if (mod.hash.compare(hash, Qt::CaseInsensitive) == 0) { + mod.matched = true; + mod.localPath = path; + match = true; + + qDebug() << "[Blocked Mods Dialog] Hash match found:" << mod.name << hash << "| From path:" << path; + + break; + } + } + + if (match) { + update(); + } +} + +/// @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) +{ + QFileInfo file = QFileInfo(path); + QString filename = file.fileName(); + + for (auto& mod : m_mods) { + if (mod.name.compare(filename, Qt::CaseInsensitive) == 0) { + qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path; + return true; + } + } + + return false; +} + +bool BlockedModsDialog::allModsMatched() +{ + 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 +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; } diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index 5f5bd61b..dac43cba 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -1,7 +1,23 @@ #pragma once #include <QDialog> +#include <QString> +#include <QList> +#include <QFileSystemWatcher> + +#include "modplatform/helpers/HashUtils.h" + +#include "tasks/ConcurrentTask.h" + +struct BlockedMod { + QString name; + QString websiteUrl; + QString hash; + bool matched; + QString localPath; + +}; QT_BEGIN_NAMESPACE namespace Ui { class BlockedModsDialog; } @@ -11,12 +27,38 @@ class BlockedModsDialog : public QDialog { Q_OBJECT public: - BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, const QString &body, const QList<QUrl> &urls); + BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, QList<BlockedMod> &mods); ~BlockedModsDialog() override; +protected: + void dragEnterEvent(QDragEnterEvent *event) override; + void dropEvent(QDropEvent *event) override; + private: Ui::BlockedModsDialog *ui; - const QList<QUrl> &urls; + 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, 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(); }; + +QDebug operator<<(QDebug debug, const BlockedMod &m); diff --git a/launcher/ui/dialogs/BlockedModsDialog.ui b/launcher/ui/dialogs/BlockedModsDialog.ui index f4ae95b6..88105178 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.ui +++ b/launcher/ui/dialogs/BlockedModsDialog.ui @@ -13,29 +13,41 @@ <property name="windowTitle"> <string notr="true">BlockedModsDialog</string> </property> - <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="0"> - <widget class="QLabel" name="label"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <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 row="2" column="0"> - <widget class="QDialogButtonBox" name="buttonBox"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> + <item> + <widget class="QLabel" name="labelExplain"> + <property name="text"> + <string/> </property> - <property name="standardButtons"> - <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> </property> </widget> </item> - <item row="1" column="0"> - <widget class="QTextBrowser" name="textBrowser"> + <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> @@ -44,6 +56,68 @@ </property> </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"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </item> </layout> </widget> <resources/> diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp index 05269f62..da73a449 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -44,7 +44,8 @@ void ProgressDialog::setSkipButton(bool present, QString label) void ProgressDialog::on_skipButton_clicked(bool checked) { Q_UNUSED(checked); - task->abort(); + if (ui->skipButton->isEnabled()) // prevent other triggers from aborting + task->abort(); } ProgressDialog::~ProgressDialog() diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 6661bf0f..cae0635f 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -303,21 +303,27 @@ void LauncherPage::applySettings() s->set("IconTheme", "pe_blue"); break; case 4: - s->set("IconTheme", "OSX"); + s->set("IconTheme", "breeze_light"); break; case 5: - s->set("IconTheme", "iOS"); + s->set("IconTheme", "breeze_dark"); break; case 6: - s->set("IconTheme", "flat"); + s->set("IconTheme", "OSX"); break; case 7: - s->set("IconTheme", "flat_white"); + s->set("IconTheme", "iOS"); break; case 8: - s->set("IconTheme", "multimc"); + s->set("IconTheme", "flat"); break; case 9: + s->set("IconTheme", "flat_white"); + break; + case 10: + s->set("IconTheme", "multimc"); + break; + case 11: s->set("IconTheme", "custom"); break; } @@ -397,7 +403,18 @@ void LauncherPage::loadSettings() m_currentUpdateChannel = s->get("UpdateChannel").toString(); //FIXME: make generic auto theme = s->get("IconTheme").toString(); - QStringList iconThemeOptions{"pe_colored", "pe_light", "pe_dark", "pe_blue", "OSX", "iOS", "flat", "flat_white", "multimc", "custom"}; + QStringList iconThemeOptions{"pe_colored", + "pe_light", + "pe_dark", + "pe_blue", + "breeze_light", + "breeze_dark", + "OSX", + "iOS", + "flat", + "flat_white", + "multimc", + "custom"}; ui->themeComboBox->setCurrentIndex(iconThemeOptions.indexOf(theme)); auto cat = s->get("BackgroundCat").toString(); diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 6de644ee..c44718a1 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -287,6 +287,16 @@ </item> <item> <property name="text"> + <string>Breeze Light</string> + </property> + </item> + <item> + <property name="text"> + <string>Breeze Dark</string> + </property> + </item> + <item> + <property name="text"> <string notr="true">OSX</string> </property> </item> diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index b6c873cc..5c919573 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -14,8 +14,6 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared { ui->setupUi(this); - ExternalResourcesPage::runningStateChanged(m_instance && m_instance->isRunning()); - ui->actionsToolbar->insertSpacer(ui->actionViewConfigs); m_filterModel = model->createFilterProxyModel(this); @@ -45,7 +43,6 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared auto selection_model = ui->treeView->selectionModel(); connect(selection_model, &QItemSelectionModel::currentChanged, this, &ExternalResourcesPage::current); connect(ui->filterEdit, &QLineEdit::textChanged, this, &ExternalResourcesPage::filterTextChanged); - connect(m_instance, &BaseInstance::runningStatusChanged, this, &ExternalResourcesPage::runningStateChanged); } ExternalResourcesPage::~ExternalResourcesPage() @@ -97,14 +94,6 @@ void ExternalResourcesPage::filterTextChanged(const QString& newContents) m_filterModel->setFilterRegularExpression(m_viewFilter); } -void ExternalResourcesPage::runningStateChanged(bool running) -{ - if (m_controlsEnabled == !running) - return; - - m_controlsEnabled = !running; -} - bool ExternalResourcesPage::shouldDisplay() const { return true; diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h index 8e352cef..11058bf6 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.h +++ b/launcher/ui/pages/instance/ExternalResourcesPage.h @@ -47,7 +47,6 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { protected slots: void itemActivated(const QModelIndex& index); void filterTextChanged(const QString& newContents); - virtual void runningStateChanged(bool running); virtual void addItem(); virtual void removeItem(); diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index 76f8ec18..33a03336 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -27,11 +27,7 @@ <item row="4" column="1" colspan="3"> <layout class="QGridLayout" name="gridLayout_2"> <item row="0" column="1"> - <widget class="QLineEdit" name="filterEdit"> - <property name="clearButtonEnabled"> - <bool>true</bool> - </property> - </widget> + <widget class="QLineEdit" name="filterEdit"/> </item> <item row="0" column="0"> <widget class="QLabel" name="filterLabel"> diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index f0106066..0a2e6155 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -108,13 +108,13 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> disconnect(mods.get(), &ModFolderModel::updateFinished, this, 0); }); + connect(m_instance, &BaseInstance::runningStatusChanged, this, &ModFolderPage::runningStateChanged); ModFolderPage::runningStateChanged(m_instance && m_instance->isRunning()); } } void ModFolderPage::runningStateChanged(bool running) { - ExternalResourcesPage::runningStateChanged(running); ui->actionDownloadItem->setEnabled(!running); ui->actionUpdateItem->setEnabled(!running); ui->actionAddItem->setEnabled(!running); diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index c9a55bde..f20adf34 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -53,12 +53,12 @@ class ModFolderPage : public ExternalResourcesPage { virtual QString helpPage() const override { return "Loader-mods"; } virtual bool shouldDisplay() const override; - void runningStateChanged(bool running) override; public slots: bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override; private slots: + void runningStateChanged(bool running); void removeItem() override; void installMods(); 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/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui index fcba5598..14b7cd9f 100644 --- a/launcher/ui/pages/instance/VersionPage.ui +++ b/launcher/ui/pages/instance/VersionPage.ui @@ -48,11 +48,7 @@ <item> <layout class="QGridLayout" name="gridLayout_2"> <item row="0" column="1"> - <widget class="QLineEdit" name="filterEdit"> - <property name="clearButtonEnabled"> - <bool>true</bool> - </property> - </widget> + <widget class="QLineEdit" name="filterEdit"/> </item> <item row="0" column="0"> <widget class="QLabel" name="filterLabel"> 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) |