diff options
Diffstat (limited to 'launcher')
58 files changed, 1051 insertions, 262 deletions
diff --git a/launcher/FileIgnoreProxy.cpp b/launcher/FileIgnoreProxy.cpp index 4c8c64c7..df06c3c7 100644 --- a/launcher/FileIgnoreProxy.cpp +++ b/launcher/FileIgnoreProxy.cpp @@ -267,10 +267,7 @@ bool FileIgnoreProxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceP  bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const  { -    auto fileName = fileInfo.fileName(); -    auto path = relPath(fileInfo.absoluteFilePath()); -    return std::any_of(m_ignoreFiles.cbegin(), m_ignoreFiles.cend(), [fileName](auto iFileName) { return fileName == iFileName; }) || -           m_ignoreFilePaths.covers(path); +    return m_ignoreFiles.contains(fileInfo.fileName()) || m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath()));  }  bool FileIgnoreProxy::filterFile(const QString& fileName) const diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index d7dabe07..0e64c46d 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -195,6 +195,12 @@ void MinecraftInstance::loadSpecificSettings()      m_settings->registerSetting("UseAccountForInstance", false);      m_settings->registerSetting("InstanceAccountId", ""); +    m_settings->registerSetting("ExportName", ""); +    m_settings->registerSetting("ExportVersion", "1.0.0"); +    m_settings->registerSetting("ExportSummary", ""); +    m_settings->registerSetting("ExportAuthor", ""); +    m_settings->registerSetting("ExportOptionalFiles", true); +      qDebug() << "Instance-type specific settings were loaded!";      setSpecificSettingsLoaded(true); diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index ae3dea8d..31094637 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -132,17 +132,23 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata, bool attempt_trash) -      if (!preserve_metadata) {          qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); -        if (metadata()) { -            Metadata::remove(index_dir, metadata()->slug); -        } else { -            auto n = name(); -            Metadata::remove(index_dir, n); -        } +        destroyMetadata(index_dir);      }      return Resource::destroy(attempt_trash);  } +void Mod::destroyMetadata(QDir& index_dir) +{ +    if (metadata()) { +        Metadata::remove(index_dir, metadata()->slug); +    } else { +        auto n = name(); +        Metadata::remove(index_dir, n); +    } +    m_local_details.metadata = nullptr; +} +  auto Mod::details() const -> const ModDetails&  {      return m_local_details; @@ -246,7 +252,8 @@ void Mod::setIcon(QImage new_image) const          PixmapCache::remove(m_pack_image_cache_key.key);      // scale the image to avoid flooding the pixmapcache -    auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding)); +    auto pixmap = +        QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation));      m_pack_image_cache_key.key = PixmapCache::insert(pixmap);      m_pack_image_cache_key.was_ever_used = true; @@ -259,7 +266,7 @@ QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const      if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) {          if (size.isNull())              return cached_image; -        return cached_image.scaled(size, mode); +        return cached_image.scaled(size, mode, Qt::SmoothTransformation);      }      // No valid image we can get diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 6dafecfc..e97ee9d3 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -93,6 +93,8 @@ class Mod : public Resource {      // Delete all the files of this mod      auto destroy(QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; +    // Delete the metadata only +    void destroyMetadata(QDir& index_dir);      void finishResolvingWithDetails(ModDetails&& details); diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index eed35615..a5f1489d 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -233,6 +233,25 @@ bool ModFolderModel::deleteMods(const QModelIndexList& indexes)      return true;  } +bool ModFolderModel::deleteModsMetadata(const QModelIndexList& indexes) +{ +    if (indexes.isEmpty()) +        return true; + +    for (auto i : indexes) { +        if (i.column() != 0) { +            continue; +        } +        auto m = at(i.row()); +        auto index_dir = indexDir(); +        m->destroyMetadata(index_dir); +    } + +    update(); + +    return true; +} +  bool ModFolderModel::isValid()  {      return m_dir.exists() && m_dir.isReadable(); diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index f1890e87..61d840f9 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -81,6 +81,7 @@ class ModFolderModel : public ResourceFolderModel {      /// Deletes all the selected mods      bool deleteMods(const QModelIndexList& indexes); +    bool deleteModsMetadata(const QModelIndexList& indexes);      bool isValid(); diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index dab0f6d6..2bb51dc5 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -50,7 +50,8 @@ void ResourcePack::setImage(QImage new_image) const          PixmapCache::instance().remove(m_pack_image_cache_key.key);      // scale the image to avoid flooding the pixmapcache -    auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding)); +    auto pixmap = +        QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation));      m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap);      m_pack_image_cache_key.was_ever_used = true; @@ -68,7 +69,7 @@ QPixmap ResourcePack::image(QSize size, Qt::AspectRatioMode mode) const      if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) {          if (size.isNull())              return cached_image; -        return cached_image.scaled(size, mode); +        return cached_image.scaled(size, mode, Qt::SmoothTransformation);      }      // No valid image we can get diff --git a/launcher/minecraft/mod/TexturePack.cpp b/launcher/minecraft/mod/TexturePack.cpp index 7d8c6713..04cc3631 100644 --- a/launcher/minecraft/mod/TexturePack.cpp +++ b/launcher/minecraft/mod/TexturePack.cpp @@ -44,7 +44,8 @@ void TexturePack::setImage(QImage new_image) const          PixmapCache::remove(m_pack_image_cache_key.key);      // scale the image to avoid flooding the pixmapcache -    auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding)); +    auto pixmap = +        QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation));      m_pack_image_cache_key.key = PixmapCache::insert(pixmap);      m_pack_image_cache_key.was_ever_used = true; @@ -56,7 +57,7 @@ QPixmap TexturePack::image(QSize size, Qt::AspectRatioMode mode) const      if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) {          if (size.isNull())              return cached_image; -        return cached_image.scaled(size, mode); +        return cached_image.scaled(size, mode, Qt::SmoothTransformation);      }      // No valid image we can get diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index 7965d0f5..3b195938 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -105,7 +105,9 @@ class ResourceAPI {          void operator=(ProjectInfoArgs other) { pack = other.pack; }      };      struct ProjectInfoCallbacks { -        std::function<void(QJsonDocument&, ModPlatform::IndexedPack)> on_succeed; +        std::function<void(QJsonDocument&, const ModPlatform::IndexedPack&)> on_succeed; +        std::function<void(QString const& reason)> on_fail; +        std::function<void()> on_abort;      };      struct DependencySearchArgs { diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 47350c33..e22d8f0d 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -38,6 +38,8 @@ class FlameAPI : public NetworkResourceAPI {                  return 6;              case ModPlatform::ResourceType::RESOURCE_PACK:                  return 12; +            case ModPlatform::ResourceType::SHADER_PACK: +                return 6552;          }      } diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index 0863f0b2..d86d34bf 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -43,12 +43,14 @@ const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" });  FlamePackExportTask::FlamePackExportTask(const QString& name,                                           const QString& version,                                           const QString& author, +                                         bool optionalFiles,                                           InstancePtr instance,                                           const QString& output,                                           MMCZip::FilterFunction filter)      : name(name)      , version(version)      , author(author) +    , optionalFiles(optionalFiles)      , instance(instance)      , mcInstance(dynamic_cast<MinecraftInstance*>(instance.get()))      , gameRoot(instance->gameRoot()) @@ -410,7 +412,7 @@ QByteArray FlamePackExportTask::generateIndex()          QJsonObject file;          file["projectID"] = mod.addonId;          file["fileID"] = mod.version; -        file["required"] = mod.enabled; +        file["required"] = mod.enabled || !optionalFiles;          files << file;      }      obj["files"] = files; diff --git a/launcher/modplatform/flame/FlamePackExportTask.h b/launcher/modplatform/flame/FlamePackExportTask.h index d3dc6281..78b46e91 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.h +++ b/launcher/modplatform/flame/FlamePackExportTask.h @@ -30,6 +30,7 @@ class FlamePackExportTask : public Task {      FlamePackExportTask(const QString& name,                          const QString& version,                          const QString& author, +                        bool optionalFiles,                          InstancePtr instance,                          const QString& output,                          MMCZip::FilterFunction filter); @@ -44,6 +45,7 @@ class FlamePackExportTask : public Task {      // inputs      const QString name, version, author; +    const bool optionalFiles;      const InstancePtr instance;      MinecraftInstance* mcInstance;      const QDir gameRoot; diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp index 78b39fff..dccccdc2 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.cpp +++ b/launcher/modplatform/helpers/NetworkResourceAPI.cpp @@ -72,7 +72,8 @@ Task::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfo          callbacks.on_succeed(doc, args.pack);      }); - +    QObject::connect(job.get(), &NetJob::failed, [callbacks](QString reason) { callbacks.on_fail(reason); }); +    QObject::connect(job.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); });      return job;  } diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp index ad8fefac..a9ddb0c9 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -33,12 +33,14 @@ const QStringList ModrinthPackExportTask::FILE_EXTENSIONS({ "jar", "litemod", "z  ModrinthPackExportTask::ModrinthPackExportTask(const QString& name,                                                 const QString& version,                                                 const QString& summary, +                                               bool optionalFiles,                                                 InstancePtr instance,                                                 const QString& output,                                                 MMCZip::FilterFunction filter)      : name(name)      , version(version)      , summary(summary) +    , optionalFiles(optionalFiles)      , instance(instance)      , mcInstance(dynamic_cast<MinecraftInstance*>(instance.get()))      , gameRoot(instance->gameRoot()) @@ -270,16 +272,18 @@ QByteArray ModrinthPackExportTask::generateIndex()          QString path = iterator.key();          const ResolvedFile& value = iterator.value(); -        // detect disabled mod -        const QFileInfo pathInfo(path); -        if (pathInfo.suffix() == "disabled") { -            // rename it -            path = pathInfo.dir().filePath(pathInfo.completeBaseName()); -            // ...and make it optional -            QJsonObject env; -            env["client"] = "optional"; -            env["server"] = "optional"; -            fileOut["env"] = env; +        if (optionalFiles) { +            // detect disabled mod +            const QFileInfo pathInfo(path); +            if (pathInfo.suffix() == "disabled") { +                // rename it +                path = pathInfo.dir().filePath(pathInfo.completeBaseName()); +                // ...and make it optional +                QJsonObject env; +                env["client"] = "optional"; +                env["server"] = "optional"; +                fileOut["env"] = env; +            }          }          fileOut["path"] = path; diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h index 1f9e0eb7..83540dfa 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.h +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h @@ -31,6 +31,7 @@ class ModrinthPackExportTask : public Task {      ModrinthPackExportTask(const QString& name,                             const QString& version,                             const QString& summary, +                           bool optionalFiles,                             InstancePtr instance,                             const QString& output,                             MMCZip::FilterFunction filter); @@ -50,6 +51,7 @@ class ModrinthPackExportTask : public Task {      // inputs      const QString name, version, summary; +    const bool optionalFiles;      const InstancePtr instance;      MinecraftInstance* mcInstance;      const QDir gameRoot; diff --git a/launcher/ui/dialogs/ExportPackDialog.cpp b/launcher/ui/dialogs/ExportPackDialog.cpp index ad8db5ff..5af24b1b 100644 --- a/launcher/ui/dialogs/ExportPackDialog.cpp +++ b/launcher/ui/dialogs/ExportPackDialog.cpp @@ -37,15 +37,21 @@  ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider)      : QDialog(parent), instance(instance), ui(new Ui::ExportPackDialog), m_provider(provider)  { +    Q_ASSERT(m_provider == ModPlatform::ResourceProvider::MODRINTH || m_provider == ModPlatform::ResourceProvider::FLAME); +      ui->setupUi(this); -    ui->name->setText(instance->name()); +    ui->name->setPlaceholderText(instance->name()); +    ui->name->setText(instance->settings()->get("ExportName").toString()); +    ui->version->setText(instance->settings()->get("ExportVersion").toString()); +    ui->optionalFiles->setChecked(instance->settings()->get("ExportOptionalFiles").toBool()); +      if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { -        ui->summary->setText(instance->notes().split(QRegularExpression("\\r?\\n"))[0]); -        setWindowTitle("Export Modrinth Pack"); +        setWindowTitle(tr("Export Modrinth Pack")); +        ui->summary->setText(instance->settings()->get("ExportSummary").toString());      } else { -        setWindowTitle("Export CurseForge Pack"); -        ui->version->setText(""); -        ui->summaryLabel->setText("Author"); +        setWindowTitle(tr("Export CurseForge Pack")); +        ui->summaryLabel->setText(tr("&Author")); +        ui->summary->setText(instance->settings()->get("ExportAuthor").toString());      }      // ensure a valid pack is generated @@ -75,20 +81,19 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPla      MinecraftInstance* mcInstance = dynamic_cast<MinecraftInstance*>(instance.get());      if (mcInstance) { -        mcInstance->loaderModList()->update();          const QDir index = mcInstance->loaderModList()->indexDir();          if (index.exists()) -            proxy->blockedPaths().insert(root.relativeFilePath(index.absolutePath())); +            proxy->ignoreFilesWithPath().insert(root.relativeFilePath(index.absolutePath()));      } -    ui->treeView->setModel(proxy); -    ui->treeView->setRootIndex(proxy->mapFromSource(model->index(instance->gameRoot()))); -    ui->treeView->sortByColumn(0, Qt::AscendingOrder); +    ui->files->setModel(proxy); +    ui->files->setRootIndex(proxy->mapFromSource(model->index(instance->gameRoot()))); +    ui->files->sortByColumn(0, Qt::AscendingOrder);      model->setFilter(filter);      model->setRootPath(instance->gameRoot()); -    QHeaderView* headerView = ui->treeView->header(); +    QHeaderView* headerView = ui->files->header();      headerView->setSectionResizeMode(QHeaderView::ResizeToContents);      headerView->setSectionResizeMode(0, QHeaderView::Stretch);  } @@ -100,26 +105,41 @@ ExportPackDialog::~ExportPackDialog()  void ExportPackDialog::done(int result)  { +    auto settings = instance->settings(); +    settings->set("ExportName", ui->name->text()); +    settings->set("ExportVersion", ui->version->text()); +    settings->set(m_provider == ModPlatform::ResourceProvider::FLAME ? "ExportAuthor" : "ExportSummary", ui->summary->text()); +    settings->set("ExportOptionalFiles", ui->optionalFiles->isChecked()); +      if (result == Accepted) { -        const QString filename = FS::RemoveInvalidFilenameChars(ui->name->text()); +        const QString name = ui->name->text().isEmpty() ? instance->name() : ui->name->text(); +        const QString filename = FS::RemoveInvalidFilenameChars(name); +          QString output; -        if (m_provider == ModPlatform::ResourceProvider::MODRINTH) -            output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()), -                                                  FS::PathCombine(QDir::homePath(), filename + ".mrpack"), "Modrinth pack (*.mrpack *.zip)", -                                                  nullptr); -        else -            output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()), -                                                  FS::PathCombine(QDir::homePath(), filename + ".zip"), "CurseForge pack (*.zip)", nullptr); - -        if (output.isEmpty()) -            return; +        if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { +            output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + ".mrpack"), +                                                  "Modrinth pack (*.mrpack *.zip)", nullptr); +            if (output.isEmpty()) +                return; +            if (!(output.endsWith(".zip") || output.endsWith(".mrpack"))) +                output.append(".mrpack"); +        } else { +            output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + ".zip"), +                                                  "CurseForge pack (*.zip)", nullptr); +            if (output.isEmpty()) +                return; +            if (!output.endsWith(".zip")) +                output.append(".zip"); +        } +          Task* task; -        if (m_provider == ModPlatform::ResourceProvider::MODRINTH) -            task = new ModrinthPackExportTask(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output, -                                              std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); -        else -            task = new FlamePackExportTask(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output, +        if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { +            task = new ModrinthPackExportTask(name, ui->version->text(), ui->summary->text(), ui->optionalFiles->isChecked(), instance, +                                              output, std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); +        } else { +            task = new FlamePackExportTask(name, ui->version->text(), ui->summary->text(), ui->optionalFiles->isChecked(), instance, output,                                             std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); +        }          connect(task, &Task::failed,                  [this](const QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); @@ -140,7 +160,6 @@ void ExportPackDialog::done(int result)  void ExportPackDialog::validate()  { -    const bool invalid = -        ui->name->text().isEmpty() || ((m_provider == ModPlatform::ResourceProvider::MODRINTH) && ui->version->text().isEmpty()); -    ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(invalid); +    ui->buttonBox->button(QDialogButtonBox::Ok) +        ->setDisabled(m_provider == ModPlatform::ResourceProvider::MODRINTH && ui->version->text().isEmpty());  } diff --git a/launcher/ui/dialogs/ExportPackDialog.ui b/launcher/ui/dialogs/ExportPackDialog.ui index 3976e28f..09dea72a 100644 --- a/launcher/ui/dialogs/ExportPackDialog.ui +++ b/launcher/ui/dialogs/ExportPackDialog.ui @@ -7,12 +7,9 @@      <x>0</x>      <y>0</y>      <width>650</width> -    <height>413</height> +    <height>510</height>     </rect>    </property> -  <property name="windowTitle"> -   <string>Export Pack</string> -  </property>    <property name="sizeGripEnabled">     <bool>true</bool>    </property> @@ -20,13 +17,16 @@     <item>      <widget class="QGroupBox" name="information">       <property name="title"> -      <string>Information</string> +      <string>&Description</string>       </property>       <layout class="QGridLayout" name="gridLayout">        <item row="3" column="0">         <widget class="QLabel" name="summaryLabel">          <property name="text"> -         <string>Summary</string> +         <string>&Summary</string> +        </property> +        <property name="buddy"> +         <cstring>summary</cstring>          </property>         </widget>        </item> @@ -36,14 +36,20 @@        <item row="0" column="0">         <widget class="QLabel" name="nameLabel">          <property name="text"> -         <string>Name</string> +         <string>&Name</string> +        </property> +        <property name="buddy"> +         <cstring>name</cstring>          </property>         </widget>        </item>        <item row="1" column="0">         <widget class="QLabel" name="versionLabel">          <property name="text"> -         <string>Version</string> +         <string>&Version</string> +        </property> +        <property name="buddy"> +         <cstring>version</cstring>          </property>         </widget>        </item> @@ -57,31 +63,52 @@          </property>         </widget>        </item> -            </layout>      </widget>     </item>     <item> -    <widget class="QLabel" name="filesLabel"> -     <property name="text"> -      <string>Files</string> -     </property> -    </widget> -   </item> -   <item> -    <widget class="QTreeView" name="treeView"> -     <property name="alternatingRowColors"> -      <bool>true</bool> -     </property> -     <property name="selectionMode"> -      <enum>QAbstractItemView::ExtendedSelection</enum> -     </property> -     <property name="sortingEnabled"> -      <bool>true</bool> +    <widget class="QGroupBox" name="options"> +     <property name="title"> +      <string>&Options</string>       </property> -     <attribute name="headerStretchLastSection"> -      <bool>false</bool> -     </attribute> +     <layout class="QVBoxLayout" name="verticalLayout"> +      <item> +       <widget class="QLabel" name="filesLabel"> +        <property name="text"> +         <string>&Files</string> +        </property> +        <property name="buddy"> +         <cstring>files</cstring> +        </property> +       </widget> +      </item> +      <item> +       <widget class="QTreeView" name="files"> +        <property name="alternatingRowColors"> +         <bool>true</bool> +        </property> +        <property name="selectionMode"> +         <enum>QAbstractItemView::ExtendedSelection</enum> +        </property> +        <property name="sortingEnabled"> +         <bool>true</bool> +        </property> +        <attribute name="headerStretchLastSection"> +         <bool>false</bool> +        </attribute> +       </widget> +      </item> +      <item> +       <widget class="QCheckBox" name="optionalFiles"> +        <property name="text"> +         <string>&Mark disabled files as optional</string> +        </property> +        <property name="checked"> +         <bool>true</bool> +        </property> +       </widget> +      </item> +     </layout>      </widget>     </item>     <item> @@ -97,7 +124,8 @@    <tabstop>name</tabstop>    <tabstop>version</tabstop>    <tabstop>summary</tabstop> -  <tabstop>treeView</tabstop> +  <tabstop>files</tabstop> +  <tabstop>optionalFiles</tabstop>   </tabstops>   <resources/>   <connections> diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 9e121bb6..bf76b01e 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -370,6 +370,8 @@ QList<BasePage*> ShaderPackDownloadDialog::getPages()  {      QList<BasePage*> pages;      pages.append(ModrinthShaderPackPage::create(this, *m_instance)); +    if (APPLICATION->capabilities() & Application::SupportsFlame) +        pages.append(FlameShaderPackPage::create(this, *m_instance));      return pages;  } diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index 3c836691..ba703f77 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -168,6 +168,17 @@      <string>Go to mods home page</string>     </property>    </action> +  <action name="actionRemoveItemMetadata"> +   <property name="enabled"> +    <bool>false</bool> +   </property> +   <property name="text"> +    <string>Remove metadata</string> +   </property> +   <property name="toolTip"> +    <string>Remove mod's metadata</string> +   </property> +  </action>   </widget>   <customwidgets>    <customwidget> diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 0f5e29cb..b42bbf46 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -92,6 +92,10 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>          ui->actionsToolbar->addAction(ui->actionVisitItemPage);          connect(ui->actionVisitItemPage, &QAction::triggered, this, &ModFolderPage::visitModPages); +        ui->actionRemoveItemMetadata->setToolTip(tr("Remove mod's metadata")); +        ui->actionsToolbar->insertActionAfter(ui->actionRemoveItem, ui->actionRemoveItemMetadata); +        connect(ui->actionRemoveItemMetadata, &QAction::triggered, this, &ModFolderPage::deleteModMetadata); +          auto check_allow_update = [this] { return ui->treeView->selectionModel()->hasSelection() || !m_model->empty(); };          connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this, check_allow_update] { @@ -104,11 +108,16 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>              if (selected <= 1) {                  ui->actionVisitItemPage->setText(tr("Visit mod's page"));                  ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page")); + +                ui->actionRemoveItemMetadata->setToolTip(tr("Remove mod's metadata"));              } else {                  ui->actionVisitItemPage->setText(tr("Visit mods' pages"));                  ui->actionVisitItemPage->setToolTip(tr("Go to the pages of the selected mods")); + +                ui->actionRemoveItemMetadata->setToolTip(tr("Remove mods' metadata"));              }              ui->actionVisitItemPage->setEnabled(selected != 0); +            ui->actionRemoveItemMetadata->setEnabled(selected != 0);          });          connect(mods.get(), &ModFolderModel::rowsInserted, this, @@ -297,3 +306,24 @@ void ModFolderPage::visitModPages()              DesktopServices::openUrl(url);      }  } + +void ModFolderPage::deleteModMetadata() +{ +    auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); +    auto selectionCount = m_model->selectedMods(selection).length(); +    if (selectionCount == 0) +        return; +    if (selectionCount > 1) { +        auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), +                                                     tr("You are about to remove the metadata for %1 mods.\n" +                                                        "Are you sure?") +                                                         .arg(selectionCount), +                                                     QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) +                            ->exec(); + +        if (response != QMessageBox::Yes) +            return; +    } + +    m_model->deleteModsMetadata(selection); +} diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index a23dcae1..0c654d0d 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -61,6 +61,7 @@ class ModFolderPage : public ExternalResourcesPage {     private slots:      void removeItems(const QItemSelection& selection) override; +    void deleteModMetadata();      void installMods();      void updateMods(); diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index 0a7edb7b..cb8f1920 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -132,6 +132,32 @@ void ResourceModel::search()      if (hasActiveSearchJob())          return; +    if (m_search_term.startsWith("#")) { +        auto projectId = m_search_term.mid(1); +        if (!projectId.isEmpty()) { +            ResourceAPI::ProjectInfoCallbacks callbacks; + +            callbacks.on_fail = [this](QString reason) { +                if (!s_running_models.constFind(this).value()) +                    return; +                searchRequestFailed(reason, -1); +            }; +            callbacks.on_abort = [this] { +                if (!s_running_models.constFind(this).value()) +                    return; +                searchRequestAborted(); +            }; + +            callbacks.on_succeed = [this](auto& doc, auto& pack) { +                if (!s_running_models.constFind(this).value()) +                    return; +                searchRequestForOneSucceeded(doc); +            }; +            if (auto job = m_api->getProjectInfo({ projectId }, std::move(callbacks)); job) +                runSearchJob(job); +            return; +        } +    }      auto args{ createSearchArguments() };      auto callbacks{ createSearchCallbacks() }; @@ -189,11 +215,18 @@ void ResourceModel::loadEntry(QModelIndex& entry)          // Use default if no callbacks are set          if (!callbacks.on_succeed) -            callbacks.on_succeed = [this, entry](auto& doc, auto pack) { +            callbacks.on_succeed = [this, entry](auto& doc, auto& newpack) {                  if (!s_running_models.constFind(this).value())                      return; +                auto pack = newpack;                  infoRequestSucceeded(doc, pack, entry);              }; +        if (!callbacks.on_fail) +            callbacks.on_fail = [this](QString reason) { +                if (!s_running_models.constFind(this).value()) +                    return; +                QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project info:%1").arg(reason)); +            };          if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job)              runInfoJob(job); @@ -372,6 +405,27 @@ void ResourceModel::searchRequestSucceeded(QJsonDocument& doc)      endInsertRows();  } +void ResourceModel::searchRequestForOneSucceeded(QJsonDocument& doc) +{ +    ModPlatform::IndexedPack::Ptr pack = std::make_shared<ModPlatform::IndexedPack>(); + +    try { +        auto obj = Json::requireObject(doc); +        if (obj.contains("data")) +            obj = Json::requireObject(obj, "data"); +        loadIndexedPack(*pack, obj); +    } catch (const JSONValidationError& e) { +        qDebug() << doc; +        qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause(); +    } + +    m_search_state = SearchState::Finished; + +    beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + 1); +    m_packs.append(pack); +    endInsertRows(); +} +  void ResourceModel::searchRequestFailed([[maybe_unused]] QString reason, int network_error_code)  {      switch (network_error_code) { diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index cc813d6e..ecf4f8f7 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -149,6 +149,7 @@ class ResourceModel : public QAbstractListModel {     private:      /* Default search request callbacks */      void searchRequestSucceeded(QJsonDocument&); +    void searchRequestForOneSucceeded(QJsonDocument&);      void searchRequestFailed(QString reason, int network_error_code);      void searchRequestAborted(); diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index c087e2be..fc7d64a4 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -44,9 +44,6 @@  #include <QKeyEvent>  #include "Markdown.h" -#include "ResourceDownloadTask.h" - -#include "minecraft/MinecraftInstance.h"  #include "ui/dialogs/ResourceDownloadDialog.h"  #include "ui/pages/modplatform/ResourceModel.h" diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp index 9cd5eed5..dee3784e 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp @@ -67,9 +67,10 @@ bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParen      if (searchTerm.isEmpty()) {          return true;      } -      QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);      ATLauncher::IndexedPack pack = sourceModel()->data(index, Qt::UserRole).value<ATLauncher::IndexedPack>(); +    if (searchTerm.startsWith("#")) +        return QString::number(pack.id) == searchTerm.mid(1);      return pack.name.contains(searchTerm, Qt::CaseInsensitive);  } diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index 39f4f346..b6fb7153 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -21,6 +21,7 @@  #include <Json.h>  #include "net/ApiDownload.h" +#include "ui/widgets/ProjectItem.h"  namespace Atl { @@ -46,27 +47,50 @@ QVariant ListModel::data(const QModelIndex& index, int role) const      }      ATLauncher::IndexedPack pack = modpacks.at(pos); -    if (role == Qt::DisplayRole) { -        return pack.name; -    } else if (role == Qt::ToolTipRole) { -        return pack.name; -    } else if (role == Qt::DecorationRole) { -        if (m_logoMap.contains(pack.safeName)) { -            return (m_logoMap.value(pack.safeName)); +    switch (role) { +        case Qt::ToolTipRole: { +            if (pack.description.length() > 100) { +                // some magic to prevent to long tooltips and replace html linebreaks +                QString edit = pack.description.left(97); +                edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); +                return edit; +            } +            return pack.description;          } -        auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder"); +        case Qt::DecorationRole: { +            if (m_logoMap.contains(pack.safeName)) { +                return (m_logoMap.value(pack.safeName)); +            } +            auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder"); -        auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower()); -        ((ListModel*)this)->requestLogo(pack.safeName, url); +            auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower()); +            ((ListModel*)this)->requestLogo(pack.safeName, url); -        return icon; -    } else if (role == Qt::UserRole) { -        QVariant v; -        v.setValue(pack); -        return v; +            return icon; +        } +        case Qt::UserRole: { +            QVariant v; +            v.setValue(pack); +            return v; +        } +        case Qt::DisplayRole: +            return pack.name; +        case Qt::SizeHintRole: +            return QSize(0, 58); +        // Custom data +        case UserDataTypes::TITLE: +            return pack.name; +        case UserDataTypes::DESCRIPTION: +            return pack.description; +        case UserDataTypes::SELECTED: +            return false; +        case UserDataTypes::INSTALLED: +            return false; +        default: +            break;      } -    return QVariant(); +    return {};  }  void ListModel::request() diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index 5e3b9ecf..c7e80027 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -35,11 +35,11 @@   */  #include "AtlPage.h" +#include "ui/widgets/ProjectItem.h"  #include "ui_AtlPage.h"  #include "BuildConfig.h" -#include "AtlOptionalModDialog.h"  #include "AtlUserInteractionSupportImpl.h"  #include "modplatform/atlauncher/ATLPackInstallTask.h"  #include "ui/dialogs/NewInstanceDialog.h" @@ -71,6 +71,8 @@ AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent),      connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &AtlPage::onSortingSelectionChanged);      connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &AtlPage::onSelectionChanged);      connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &AtlPage::onVersionSelectionChanged); + +    ui->packView->setItemDelegate(new ProjectItemDelegate(this));  }  AtlPage::~AtlPage() diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui index 746aa6d1..8b674733 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui @@ -11,21 +11,28 @@     </rect>    </property>    <layout class="QGridLayout" name="gridLayout"> -   <item row="1" column="0" colspan="2"> -    <layout class="QGridLayout" name="gridLayout_3"> -     <item row="1" column="0"> -      <widget class="QTreeView" name="packView"> -       <property name="alternatingRowColors"> -        <bool>true</bool> +   <item row="3" column="0" colspan="2"> +    <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0"> +     <item row="0" column="2"> +      <widget class="QComboBox" name="versionSelectionBox"/> +     </item> +     <item row="0" column="1"> +      <widget class="QLabel" name="label"> +       <property name="text"> +        <string>Version selected:</string>         </property> -       <property name="iconSize"> -        <size> -         <width>96</width> -         <height>48</height> -        </size> +       <property name="alignment"> +        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>         </property>        </widget>       </item> +     <item row="0" column="0"> +      <widget class="QComboBox" name="sortByBox"/> +     </item> +    </layout> +   </item> +   <item row="2" column="0" colspan="2"> +    <layout class="QGridLayout" name="gridLayout_3">       <item row="1" column="1">        <widget class="QTextBrowser" name="packDescription">         <property name="openExternalLinks"> @@ -36,39 +43,22 @@         </property>        </widget>       </item> -     <item row="0" column="0" colspan="2"> -      <widget class="QLabel" name="label_2"> -       <property name="text"> -        <string>Warning: This is still a work in progress. If you run into issues with the imported modpack, it may be a bug.</string> -       </property> -       <property name="wordWrap"> +     <item row="1" column="0"> +      <widget class="QTreeView" name="packView"> +       <property name="alternatingRowColors">          <bool>true</bool>         </property> -      </widget> -     </item> -    </layout> -   </item> -   <item row="2" column="0" colspan="2"> -    <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0"> -     <item row="0" column="2"> -      <widget class="QComboBox" name="versionSelectionBox"/> -     </item> -     <item row="0" column="1"> -      <widget class="QLabel" name="label"> -       <property name="text"> -        <string>Version selected:</string> -       </property> -       <property name="alignment"> -        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> +       <property name="iconSize"> +        <size> +         <width>96</width> +         <height>48</height> +        </size>         </property>        </widget>       </item> -     <item row="0" column="0"> -      <widget class="QComboBox" name="sortByBox"/> -     </item>      </layout>     </item> -   <item row="0" column="0"> +   <item row="1" column="0">      <widget class="QLineEdit" name="searchEdit">       <property name="placeholderText">        <string>Search and filter...</string> @@ -78,6 +68,31 @@       </property>      </widget>     </item> +   <item row="1" column="1"> +    <widget class="QPushButton" name="pushButton"> +     <property name="text"> +      <string>Search</string> +     </property> +    </widget> +   </item> +   <item row="0" column="0" colspan="2"> +    <widget class="QLabel" name="label_2"> +     <property name="font"> +      <font> +       <italic>true</italic> +      </font> +     </property> +     <property name="text"> +      <string>Warning: This is still a work in progress. If you run into issues with the imported modpack, it may be a bug.</string> +     </property> +     <property name="alignment"> +      <set>Qt::AlignCenter</set> +     </property> +     <property name="wordWrap"> +      <bool>true</bool> +     </property> +    </widget> +   </item>    </layout>   </widget>   <tabstops> diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index ff21d010..8875a945 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -1,6 +1,8 @@  #include "FlameModel.h"  #include <Json.h>  #include "Application.h" +#include "modplatform/ResourceAPI.h" +#include "modplatform/flame/FlameAPI.h"  #include "ui/widgets/ProjectItem.h"  #include "net/ApiDownload.h" @@ -161,6 +163,21 @@ void ListModel::fetchMore(const QModelIndex& parent)  void ListModel::performPaginatedSearch()  { +    if (currentSearchTerm.startsWith("#")) { +        auto projectId = currentSearchTerm.mid(1); +        if (!projectId.isEmpty()) { +            ResourceAPI::ProjectInfoCallbacks callbacks; + +            callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); }; +            callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); }; +            static const FlameAPI api; +            if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { +                jobPtr = job; +                jobPtr->start(); +            } +            return; +        } +    }      auto netJob = makeShared<NetJob>("Flame::Search", APPLICATION->network());      auto searchUrl = QString(                           "https://api.curseforge.com/v1/mods/search?" @@ -189,23 +206,24 @@ void ListModel::searchWithTerm(const QString& term, int sort)      }      currentSearchTerm = term;      currentSort = sort; -    if (jobPtr) { +    if (hasActiveSearchJob()) {          jobPtr->abort();          searchState = ResetRequested;          return; -    } else { -        beginResetModel(); -        modpacks.clear(); -        endResetModel(); -        searchState = None;      } +    beginResetModel(); +    modpacks.clear(); +    endResetModel(); +    searchState = None; +      nextSearchOffset = 0;      performPaginatedSearch();  }  void Flame::ListModel::searchRequestFinished()  { -    jobPtr.reset(); +    if (hasActiveSearchJob()) +        return;      QJsonParseError parse_error;      QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); @@ -246,6 +264,25 @@ void Flame::ListModel::searchRequestFinished()      endInsertRows();  } +void Flame::ListModel::searchRequestForOneSucceeded(QJsonDocument& doc) +{ +    jobPtr.reset(); + +    auto packObj = Json::ensureObject(doc.object(), "data"); + +    Flame::IndexedPack pack; +    try { +        Flame::loadIndexedPack(pack, packObj); +    } catch (const JSONValidationError& e) { +        qWarning() << "Error while loading pack from CurseForge: " << e.cause(); +        return; +    } + +    beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + 1); +    modpacks.append({ pack }); +    endInsertRows(); +} +  void Flame::ListModel::searchRequestFailed(QString reason)  {      jobPtr.reset(); diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.h b/launcher/ui/pages/modplatform/flame/FlameModel.h index b3bc96b8..fd8496df 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModel.h @@ -40,6 +40,9 @@ class ListModel : public QAbstractListModel {      void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);      void searchWithTerm(const QString& term, const int sort); +    [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } +    [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } +     private slots:      void performPaginatedSearch(); @@ -48,6 +51,7 @@ class ListModel : public QAbstractListModel {      void searchRequestFinished();      void searchRequestFailed(QString reason); +    void searchRequestForOneSucceeded(QJsonDocument&);     private:      void requestLogo(QString file, QString url); @@ -63,7 +67,7 @@ class ListModel : public QAbstractListModel {      int currentSort = 0;      int nextSearchOffset = 0;      enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; -    NetJob::Ptr jobPtr; +    Task::Ptr jobPtr;      std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();  }; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 183e16f9..50656f42 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -50,7 +50,8 @@  static FlameAPI api; -FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog) +FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) +    : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog), m_fetch_progress(this, false)  {      ui->setupUi(this);      connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch); @@ -61,6 +62,17 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(paren      ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);      ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); +    m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); +    m_search_timer.setSingleShot(true); + +    connect(&m_search_timer, &QTimer::timeout, this, &FlamePage::triggerSearch); + +    m_fetch_progress.hideIfInactive(true); +    m_fetch_progress.setFixedHeight(24); +    m_fetch_progress.progressFormat(""); + +    ui->gridLayout->addWidget(&m_fetch_progress, 2, 0, 1, ui->gridLayout->columnCount()); +      // index is used to set the sorting with the curseforge api      ui->sortByBox->addItem(tr("Sort by Featured"));      ui->sortByBox->addItem(tr("Sort by Popularity")); @@ -90,6 +102,11 @@ bool FlamePage::eventFilter(QObject* watched, QEvent* event)              triggerSearch();              keyEvent->accept();              return true; +        } else { +            if (m_search_timer.isActive()) +                m_search_timer.stop(); + +            m_search_timer.start(350);          }      }      return QWidget::eventFilter(watched, event); @@ -114,6 +131,7 @@ void FlamePage::openedImpl()  void FlamePage::triggerSearch()  {      listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); +    m_fetch_progress.watch(listModel->activeSearchJob().get());  }  void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index ff5c7975..d35858fb 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -39,8 +39,9 @@  #include <Application.h>  #include <modplatform/flame/FlamePackIndex.h> -#include "tasks/Task.h" +#include <QTimer>  #include "ui/pages/BasePage.h" +#include "ui/widgets/ProgressWidget.h"  namespace Ui {  class FlamePage; @@ -86,4 +87,9 @@ class FlamePage : public QWidget, public BasePage {      Flame::IndexedPack current;      int m_selected_version_index = -1; + +    ProgressWidget m_fetch_progress; + +    // Used to do instant searching with a delay to cache quick changes +    QTimer m_search_timer;  }; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index 71d19513..f9e1fe67 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -47,7 +47,7 @@       </item>      </layout>     </item> -   <item row="2" column="0"> +   <item row="3" column="0">      <layout class="QHBoxLayout">       <item>        <widget class="QListView" name="packView"> @@ -77,7 +77,7 @@       </item>      </layout>     </item> -   <item row="3" column="0"> +   <item row="4" column="0">      <layout class="QHBoxLayout">       <item>        <widget class="QComboBox" name="sortByBox"/> diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index c80e4f99..7d18e72a 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -121,4 +121,27 @@ auto FlameTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonAr      return Json::ensureArray(obj.object(), "data");  } +FlameShaderPackModel::FlameShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new FlameAPI) {} + +void FlameShaderPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ +    FlameMod::loadIndexedPack(m, obj); +} + +// We already deal with the URLs when initializing the pack, due to the API response's structure +void FlameShaderPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ +    FlameMod::loadBody(m, obj); +} + +void FlameShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +{ +    FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); +} + +auto FlameShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray +{ +    return Json::ensureArray(obj.object(), "data"); +} +  }  // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 6cfd6a6f..76dbd7b3 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -68,4 +68,21 @@ class FlameTexturePackModel : public TexturePackResourceModel {      auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;  }; +class FlameShaderPackModel : public ShaderPackResourceModel { +    Q_OBJECT + +   public: +    FlameShaderPackModel(const BaseInstance&); +    ~FlameShaderPackModel() override = default; + +   private: +    [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } +    [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } + +    void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; +    void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; +    void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; +    auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; +}; +  }  // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 1403e98f..23373ec9 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -173,6 +173,45 @@ void FlameTexturePackPage::openUrl(const QUrl& url)      TexturePackResourcePage::openUrl(url);  } +FlameShaderPackPage::FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) +    : ShaderPackResourcePage(dialog, instance) +{ +    m_model = new FlameShaderPackModel(instance); +    m_ui->packView->setModel(m_model); + +    addSortings(); + +    // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, +    // so it's best not to connect them in the parent's constructor... +    connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); +    connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameShaderPackPage::onSelectionChanged); +    connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameShaderPackPage::onVersionSelectionChanged); +    connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameShaderPackPage::onResourceSelected); + +    m_ui->packDescription->setMetaEntry(metaEntryBase()); +} + +bool FlameShaderPackPage::optedOut(ModPlatform::IndexedVersion& ver) const +{ +    return isOptedOut(ver); +} + +void FlameShaderPackPage::openUrl(const QUrl& url) +{ +    if (url.scheme().isEmpty()) { +        QString query = url.query(QUrl::FullyDecoded); + +        if (query.startsWith("remoteUrl=")) { +            // attempt to resolve url from warning page +            query.remove(0, 10); +            ShaderPackResourcePage::openUrl({ QUrl::fromPercentEncoding(query.toUtf8()) });  // double decoding is necessary +            return; +        } +    } + +    ShaderPackResourcePage::openUrl(url); +} +  // I don't know why, but doing this on the parent class makes it so that  // other mod providers start loading before being selected, at least with  // my Qt, so we need to implement this in every derived class... @@ -188,5 +227,9 @@ auto FlameTexturePackPage::shouldDisplay() const -> bool  {      return true;  } +auto FlameShaderPackPage::shouldDisplay() const -> bool +{ +    return true; +}  }  // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index 035da2d5..f2f5ceca 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -44,6 +44,7 @@  #include "ui/pages/modplatform/ModPage.h"  #include "ui/pages/modplatform/ResourcePackPage.h" +#include "ui/pages/modplatform/ShaderPackPage.h"  #include "ui/pages/modplatform/TexturePackPage.h"  namespace ResourceDownload { @@ -155,4 +156,31 @@ class FlameTexturePackPage : public TexturePackResourcePage {      void openUrl(const QUrl& url) override;  }; +class FlameShaderPackPage : public ShaderPackResourcePage { +    Q_OBJECT + +   public: +    static FlameShaderPackPage* create(ShaderPackDownloadDialog* dialog, BaseInstance& instance) +    { +        return ShaderPackResourcePage::create<FlameShaderPackPage>(dialog, instance); +    } + +    FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance); +    ~FlameShaderPackPage() override = default; + +    [[nodiscard]] bool shouldDisplay() const override; + +    [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } +    [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } +    [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } +    [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } +    [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + +    [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + +    bool optedOut(ModPlatform::IndexedVersion& ver) const override; + +    void openUrl(const QUrl& url) override; +}; +  }  // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp index 5c9ff63b..d3ead083 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp @@ -17,6 +17,7 @@   */  #include "ImportFTBPage.h" +#include "ui/widgets/ProjectItem.h"  #include "ui_ImportFTBPage.h"  #include <QWidget> @@ -32,17 +33,30 @@ ImportFTBPage::ImportFTBPage(NewInstanceDialog* dialog, QWidget* parent) : QWidg      ui->setupUi(this);      { +        currentModel = new FilterModel(this);          listModel = new ListModel(this); +        currentModel->setSourceModel(listModel); -        ui->modpackList->setModel(listModel); +        ui->modpackList->setModel(currentModel);          ui->modpackList->setSortingEnabled(true);          ui->modpackList->header()->hide();          ui->modpackList->setIndentation(0);          ui->modpackList->setIconSize(QSize(42, 42)); + +        for (int i = 0; i < currentModel->getAvailableSortings().size(); i++) { +            ui->sortByBox->addItem(currentModel->getAvailableSortings().keys().at(i)); +        } + +        ui->sortByBox->setCurrentText(currentModel->translateCurrentSorting());      }      connect(ui->modpackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &ImportFTBPage::onPublicPackSelectionChanged); +    connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &ImportFTBPage::onSortingSelectionChanged); + +    connect(ui->searchEdit, &QLineEdit::textChanged, this, &ImportFTBPage::triggerSearch); + +    ui->modpackList->setItemDelegate(new ProjectItemDelegate(this));      ui->modpackList->selectionModel()->reset();  } @@ -86,7 +100,7 @@ void ImportFTBPage::onPublicPackSelectionChanged(QModelIndex now, QModelIndex pr          onPackSelectionChanged();          return;      } -    Modpack selectedPack = listModel->data(now, Qt::UserRole).value<Modpack>(); +    Modpack selectedPack = currentModel->data(now, Qt::UserRole).value<Modpack>();      onPackSelectionChanged(&selectedPack);  } @@ -101,4 +115,15 @@ void ImportFTBPage::onPackSelectionChanged(Modpack* pack)          dialog->setSuggestedPack();  } +void ImportFTBPage::onSortingSelectionChanged(QString sort) +{ +    FilterModel::Sorting toSet = currentModel->getAvailableSortings().value(sort); +    currentModel->setSorting(toSet); +} + +void ImportFTBPage::triggerSearch() +{ +    currentModel->setSearchTerm(ui->searchEdit->text()); +} +  }  // namespace FTBImportAPP diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h index 54c49f7b..8e966127 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h @@ -53,12 +53,15 @@ class ImportFTBPage : public QWidget, public BasePage {      void suggestCurrent();      void onPackSelectionChanged(Modpack* pack = nullptr);     private slots: +    void onSortingSelectionChanged(QString data);      void onPublicPackSelectionChanged(QModelIndex first, QModelIndex second); +    void triggerSearch();     private:      bool initialized = false;      Modpack selected;      ListModel* listModel = nullptr; +    FilterModel* currentModel = nullptr;      NewInstanceDialog* dialog = nullptr;      Ui::ImportFTBPage* ui = nullptr; diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui index 32d548b0..5e09fb6d 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui @@ -10,8 +10,8 @@      <height>1011</height>     </rect>    </property> -  <layout class="QHBoxLayout" name="horizontalLayout"> -   <item> +  <layout class="QGridLayout" name="gridLayout"> +   <item row="1" column="1">      <widget class="QTreeView" name="modpackList">       <property name="maximumSize">        <size> @@ -21,6 +21,54 @@       </property>      </widget>     </item> +   <item row="0" column="1"> +    <layout class="QHBoxLayout" name="horizontalLayout"> +     <item> +      <widget class="QLineEdit" name="searchEdit"> +       <property name="placeholderText"> +        <string>Search and filter...</string> +       </property> +       <property name="clearButtonEnabled"> +        <bool>true</bool> +       </property> +      </widget> +     </item> +     <item> +      <widget class="QPushButton" name="pushButton"> +       <property name="text"> +        <string>Search</string> +       </property> +      </widget> +     </item> +    </layout> +   </item> +   <item row="2" column="1"> +    <layout class="QHBoxLayout" name="horizontalLayout_2"> +     <item> +      <widget class="QComboBox" name="sortByBox"> +       <property name="minimumSize"> +        <size> +         <width>265</width> +         <height>0</height> +        </size> +       </property> +      </widget> +     </item> +     <item> +      <spacer name="horizontalSpacer"> +       <property name="orientation"> +        <enum>Qt::Horizontal</enum> +       </property> +       <property name="sizeHint" stdset="0"> +        <size> +         <width>40</width> +         <height>20</height> +        </size> +       </property> +      </spacer> +     </item> +    </layout> +   </item>    </layout>   </widget>   <resources/> diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp index dc78f451..134bdc0c 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp @@ -23,7 +23,9 @@  #include <QIcon>  #include <QProcessEnvironment>  #include "FileSystem.h" +#include "StringUtils.h"  #include "modplatform/import_ftb/PackHelpers.h" +#include "ui/widgets/ProjectItem.h"  namespace FTBImportAPP { @@ -71,18 +73,99 @@ QVariant ListModel::data(const QModelIndex& index, int role) const      }      auto pack = modpacks.at(pos); -    if (role == Qt::DisplayRole) { -        return pack.name; -    } else if (role == Qt::DecorationRole) { -        return pack.icon; -    } else if (role == Qt::UserRole) { -        QVariant v; -        v.setValue(pack); -        return v; -    } else if (role == Qt::ToolTipRole) { -        return tr("Minecraft %1").arg(pack.mcVersion); +    if (role == Qt::ToolTipRole) {      } -    return QVariant(); +    switch (role) { +        case Qt::ToolTipRole: +            return tr("Minecraft %1").arg(pack.mcVersion); +        case Qt::DecorationRole: +            return pack.icon; +        case Qt::UserRole: { +            QVariant v; +            v.setValue(pack); +            return v; +        } +        case Qt::DisplayRole: +            return pack.name; +        case Qt::SizeHintRole: +            return QSize(0, 58); +        // Custom data +        case UserDataTypes::TITLE: +            return pack.name; +        case UserDataTypes::DESCRIPTION: +            return tr("Minecraft %1").arg(pack.mcVersion); +        case UserDataTypes::SELECTED: +            return false; +        case UserDataTypes::INSTALLED: +            return false; +        default: +            break; +    } + +    return {}; +} + +FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) +{ +    currentSorting = Sorting::ByGameVersion; +    sortings.insert(tr("Sort by Name"), Sorting::ByName); +    sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion); +} + +bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const +{ +    Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<Modpack>(); +    Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<Modpack>(); + +    if (currentSorting == Sorting::ByGameVersion) { +        Version lv(leftPack.mcVersion); +        Version rv(rightPack.mcVersion); +        return lv < rv; + +    } else if (currentSorting == Sorting::ByName) { +        return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; +    } + +    // UHM, some inavlid value set?! +    qWarning() << "Invalid sorting set!"; +    return true; +} + +bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const +{ +    if (searchTerm.isEmpty()) { +        return true; +    } +    QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); +    Modpack pack = sourceModel()->data(index, Qt::UserRole).value<Modpack>(); +    return pack.name.contains(searchTerm, Qt::CaseInsensitive); +} + +void FilterModel::setSearchTerm(const QString term) +{ +    searchTerm = term.trimmed(); +    invalidate(); +} + +const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings() +{ +    return sortings; +} + +QString FilterModel::translateCurrentSorting() +{ +    return sortings.key(currentSorting); +} + +void FilterModel::setSorting(Sorting s) +{ +    currentSorting = s; +    invalidate(); +} + +FilterModel::Sorting FilterModel::getCurrentSorting() +{ +    return currentSorting;  }  }  // namespace FTBImportAPP
\ No newline at end of file diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.h b/launcher/ui/pages/modplatform/import_ftb/ListModel.h index c67aa896..11192827 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.h +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.h @@ -20,11 +20,33 @@  #include <QAbstractListModel>  #include <QIcon> +#include <QSortFilterProxyModel>  #include <QVariant>  #include "modplatform/import_ftb/PackHelpers.h"  namespace FTBImportAPP { +class FilterModel : public QSortFilterProxyModel { +    Q_OBJECT +   public: +    FilterModel(QObject* parent = Q_NULLPTR); +    enum Sorting { ByName, ByGameVersion }; +    const QMap<QString, Sorting> getAvailableSortings(); +    QString translateCurrentSorting(); +    void setSorting(Sorting sorting); +    Sorting getCurrentSorting(); +    void setSearchTerm(QString term); + +   protected: +    bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; +    bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; + +   private: +    QMap<QString, Sorting> sortings; +    Sorting currentSorting; +    QString searchTerm; +}; +  class ListModel : public QAbstractListModel {      Q_OBJECT diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 356d919d..49666cf6 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -41,6 +41,7 @@  #include <Version.h>  #include "StringUtils.h" +#include "ui/widgets/ProjectItem.h"  #include <QLabel>  #include <QtMath> @@ -79,7 +80,20 @@ bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) co  bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const  { -    return true; +    if (searchTerm.isEmpty()) { +        return true; +    } +    QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); +    Modpack pack = sourceModel()->data(index, Qt::UserRole).value<Modpack>(); +    if (searchTerm.startsWith("#")) +        return pack.packCode == searchTerm.mid(1); +    return pack.name.contains(searchTerm, Qt::CaseInsensitive); +} + +void FilterModel::setSearchTerm(const QString term) +{ +    searchTerm = term.trimmed(); +    invalidate();  }  const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings() @@ -139,39 +153,57 @@ QVariant ListModel::data(const QModelIndex& index, int role) const      }      Modpack pack = modpacks.at(pos); -    if (role == Qt::DisplayRole) { -        return pack.name + "\n" + translatePackType(pack.type); -    } else if (role == Qt::ToolTipRole) { -        if (pack.description.length() > 100) { -            // some magic to prevent to long tooltips and replace html linebreaks -            QString edit = pack.description.left(97); -            edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); -            return edit; +    switch (role) { +        case Qt::ToolTipRole: { +            if (pack.description.length() > 100) { +                // some magic to prevent to long tooltips and replace html linebreaks +                QString edit = pack.description.left(97); +                edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); +                return edit; +            } +            return pack.description; +        } +        case Qt::DecorationRole: { +            if (m_logoMap.contains(pack.logo)) { +                return (m_logoMap.value(pack.logo)); +            } +            QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); +            ((ListModel*)this)->requestLogo(pack.logo); +            return icon;          } -        return pack.description; -    } else if (role == Qt::DecorationRole) { -        if (m_logoMap.contains(pack.logo)) { -            return (m_logoMap.value(pack.logo)); +        case Qt::UserRole: { +            QVariant v; +            v.setValue(pack); +            return v;          } -        QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); -        ((ListModel*)this)->requestLogo(pack.logo); -        return icon; -    } else if (role == Qt::ForegroundRole) { -        if (pack.broken) { -            // FIXME: Hardcoded color -            return QColor(255, 0, 50); -        } else if (pack.bugged) { -            // FIXME: Hardcoded color -            // bugged pack, currently only indicates bugged xml -            return QColor(244, 229, 66); +        case Qt::ForegroundRole: { +            if (pack.broken) { +                // FIXME: Hardcoded color +                return QColor(255, 0, 50); +            } else if (pack.bugged) { +                // FIXME: Hardcoded color +                // bugged pack, currently only indicates bugged xml +                return QColor(244, 229, 66); +            }          } -    } else if (role == Qt::UserRole) { -        QVariant v; -        v.setValue(pack); -        return v; +        case Qt::DisplayRole: +            return pack.name; +        case Qt::SizeHintRole: +            return QSize(0, 58); +        // Custom data +        case UserDataTypes::TITLE: +            return pack.name; +        case UserDataTypes::DESCRIPTION: +            return pack.description; +        case UserDataTypes::SELECTED: +            return false; +        case UserDataTypes::INSTALLED: +            return false; +        default: +            break;      } -    return QVariant(); +    return {};  }  void ListModel::fill(ModpackList modpacks_) diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h index 51a58d99..c802a4b5 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h @@ -25,6 +25,7 @@ class FilterModel : public QSortFilterProxyModel {      QString translateCurrentSorting();      void setSorting(Sorting sorting);      Sorting getCurrentSorting(); +    void setSearchTerm(QString term);     protected:      bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; @@ -33,6 +34,7 @@ class FilterModel : public QSortFilterProxyModel {     private:      QMap<QString, Sorting> sortings;      Sorting currentSorting; +    QString searchTerm;  };  class ListModel : public QAbstractListModel { diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 0103bbaa..4104f139 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -35,6 +35,7 @@   */  #include "Page.h" +#include "ui/widgets/ProjectItem.h"  #include "ui_Page.h"  #include <QInputDialog> @@ -110,6 +111,8 @@ Page::Page(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog      connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &Page::onSortingSelectionChanged);      connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &Page::onVersionSelectionItemChanged); +    connect(ui->searchEdit, &QLineEdit::textChanged, this, &Page::triggerSearch); +      connect(ui->publicPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPublicPackSelectionChanged);      connect(ui->thirdPartyPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onThirdPartyPackSelectionChanged);      connect(ui->privatePackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPrivatePackSelectionChanged); @@ -125,6 +128,9 @@ Page::Page(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog      ui->thirdPartyPackList->selectionModel()->reset();      ui->privatePackList->selectionModel()->reset(); +    ui->publicPackList->setItemDelegate(new ProjectItemDelegate(this)); +    ui->thirdPartyPackList->setItemDelegate(new ProjectItemDelegate(this)); +    ui->privatePackList->setItemDelegate(new ProjectItemDelegate(this));      onTabChanged(ui->tabWidget->currentIndex());  } @@ -319,6 +325,8 @@ void Page::onTabChanged(int tab)          currentModpackInfo = ui->publicPackDescription;      } +    triggerSearch(); +      currentList->selectionModel()->reset();      QModelIndex idx = currentList->currentIndex();      if (idx.isValid()) { @@ -358,4 +366,9 @@ void Page::onRemovePackClicked()      onPackSelectionChanged();  } +void Page::triggerSearch() +{ +    currentModel->setSearchTerm(ui->searchEdit->text()); +} +  }  // namespace LegacyFTB diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h index a12b0745..4d317b7c 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -43,7 +43,6 @@  #include "QObjectPtr.h"  #include "modplatform/legacy_ftb/PackFetchTask.h"  #include "modplatform/legacy_ftb/PackHelpers.h" -#include "tasks/Task.h"  #include "ui/pages/BasePage.h"  class NewInstanceDialog; @@ -56,8 +55,6 @@ class Page;  class ListModel;  class FilterModel; -class PrivatePackListModel; -class PrivatePackFilterModel;  class PrivatePackManager;  class Page : public QWidget, public BasePage { @@ -98,6 +95,8 @@ class Page : public QWidget, public BasePage {      void onAddPackClicked();      void onRemovePackClicked(); +    void triggerSearch(); +     private:      FilterModel* currentModel = nullptr;      QTreeView* currentList = nullptr; diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui index ad08dc25..56cba748 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui @@ -10,8 +10,29 @@      <height>602</height>     </rect>    </property> -  <layout class="QVBoxLayout" name="verticalLayout"> -   <item> +  <layout class="QGridLayout" name="gridLayout_5"> +   <item row="0" column="0"> +    <layout class="QHBoxLayout" name="horizontalLayout"> +     <item> +      <widget class="QLineEdit" name="searchEdit"> +       <property name="placeholderText"> +        <string>Search and filter...</string> +       </property> +       <property name="clearButtonEnabled"> +        <bool>true</bool> +       </property> +      </widget> +     </item> +     <item> +      <widget class="QPushButton" name="pushButton"> +       <property name="text"> +        <string>Search</string> +       </property> +      </widget> +     </item> +    </layout> +   </item> +   <item row="4" column="0">      <widget class="QTabWidget" name="tabWidget">       <property name="currentIndex">        <number>0</number> @@ -36,9 +57,9 @@         </item>         <item row="0" column="1">          <widget class="QTextBrowser" name="publicPackDescription"> -           <property name="openExternalLinks"> -            <bool>true</bool> -           </property> +         <property name="openExternalLinks"> +          <bool>true</bool> +         </property>          </widget>         </item>        </layout> @@ -50,10 +71,10 @@        <layout class="QGridLayout" name="gridLayout_3">         <item row="0" column="1">          <widget class="QTextBrowser" name="thirdPartyPackDescription"> -               <property name="openExternalLinks"> -            <bool>true</bool> -           </property> -          </widget> +         <property name="openExternalLinks"> +          <bool>true</bool> +         </property> +        </widget>         </item>         <item row="0" column="0">          <widget class="QTreeView" name="thirdPartyPackList"> @@ -104,16 +125,16 @@         </item>         <item row="0" column="1" rowspan="3">          <widget class="QTextBrowser" name="privatePackDescription"> -           <property name="openExternalLinks"> -            <bool>true</bool> -           </property> -          </widget> +         <property name="openExternalLinks"> +          <bool>true</bool> +         </property> +        </widget>         </item>        </layout>       </widget>      </widget>     </item> -   <item> +   <item row="5" column="0">      <layout class="QGridLayout" name="gridLayout_4">       <item row="0" column="1">        <widget class="QLabel" name="label"> diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index ebc5556c..f691a185 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -38,8 +38,8 @@  #include "BuildConfig.h"  #include "Json.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" +#include "modplatform/modrinth/ModrinthAPI.h" +#include "net/NetJob.h"  #include "ui/widgets/ProjectItem.h"  #include "net/ApiDownload.h" @@ -130,7 +130,24 @@ bool ModpackListModel::setData(const QModelIndex& index, const QVariant& value,  void ModpackListModel::performPaginatedSearch()  { -    // TODO: Move to standalone API +    if (hasActiveSearchJob()) +        return; + +    if (currentSearchTerm.startsWith("#")) { +        auto projectId = currentSearchTerm.mid(1); +        if (!projectId.isEmpty()) { +            ResourceAPI::ProjectInfoCallbacks callbacks; + +            callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); }; +            callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); }; +            static const ModrinthAPI api; +            if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { +                jobPtr = job; +                jobPtr->start(); +            } +            return; +        } +    }  // TODO: Move to standalone API      auto netJob = makeShared<NetJob>("Modrinth::SearchModpack", APPLICATION->network());      auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL +                                  "/search?" @@ -167,16 +184,17 @@ void ModpackListModel::performPaginatedSearch()  void ModpackListModel::refresh()  { -    if (jobPtr) { +    if (hasActiveSearchJob()) {          jobPtr->abort();          searchState = ResetRequested;          return; -    } else { -        beginResetModel(); -        modpacks.clear(); -        endResetModel(); -        searchState = None;      } + +    beginResetModel(); +    modpacks.clear(); +    endResetModel(); +    searchState = None; +      nextSearchOffset = 0;      performPaginatedSearch();  } @@ -307,9 +325,29 @@ void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all)      endInsertRows();  } +void ModpackListModel::searchRequestForOneSucceeded(QJsonDocument& doc) +{ +    jobPtr.reset(); + +    auto packObj = doc.object(); + +    Modrinth::Modpack pack; +    try { +        Modrinth::loadIndexedPack(pack, packObj); +        pack.id = Json::ensureString(packObj, "id", pack.id); +    } catch (const JSONValidationError& e) { +        qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause(); +        return; +    } + +    beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + 1); +    modpacks.append({ pack }); +    endInsertRows(); +} +  void ModpackListModel::searchRequestFailed(QString reason)  { -    auto failed_action = jobPtr->getFailedActions().at(0); +    auto failed_action = dynamic_cast<NetJob*>(jobPtr.get())->getFailedActions().at(0);      if (!failed_action->m_reply) {          // Network error          QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks.")); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 721c69f5..2a9d6226 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -73,6 +73,9 @@ class ModpackListModel : public QAbstractListModel {      void refresh();      void searchWithTerm(const QString& term, const int sort); +    [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } +    [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } +      void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);      inline auto canFetchMore(const QModelIndex& parent) const -> bool override @@ -83,6 +86,7 @@ class ModpackListModel : public QAbstractListModel {     public slots:      void searchRequestFinished(QJsonDocument& doc_all);      void searchRequestFailed(QString reason); +    void searchRequestForOneSucceeded(QJsonDocument&);     protected slots: @@ -111,7 +115,7 @@ class ModpackListModel : public QAbstractListModel {      int nextSearchOffset = 0;      enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; -    NetJob::Ptr jobPtr; +    Task::Ptr jobPtr;      std::shared_ptr<QByteArray> m_all_response = std::make_shared<QByteArray>();      QByteArray m_specific_response; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 41fd5003..f7fa8fd7 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -52,7 +52,8 @@  #include <QKeyEvent>  #include <QPushButton> -ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog) +ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) +    : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog), m_fetch_progress(this, false)  {      ui->setupUi(this); @@ -64,6 +65,17 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget      ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);      ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); +    m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); +    m_search_timer.setSingleShot(true); + +    connect(&m_search_timer, &QTimer::timeout, this, &ModrinthPage::triggerSearch); + +    m_fetch_progress.hideIfInactive(true); +    m_fetch_progress.setFixedHeight(24); +    m_fetch_progress.progressFormat(""); + +    ui->gridLayout->addWidget(&m_fetch_progress, 2, 0, 1, ui->gridLayout->columnCount()); +      ui->sortByBox->addItem(tr("Sort by Relevance"));      ui->sortByBox->addItem(tr("Sort by Total Downloads"));      ui->sortByBox->addItem(tr("Sort by Follows")); @@ -102,6 +114,11 @@ bool ModrinthPage::eventFilter(QObject* watched, QEvent* event)              this->triggerSearch();              keyEvent->accept();              return true; +        } else { +            if (m_search_timer.isActive()) +                m_search_timer.stop(); + +            m_search_timer.start(350);          }      }      return QObject::eventFilter(watched, event); @@ -309,6 +326,7 @@ void ModrinthPage::suggestCurrent()  void ModrinthPage::triggerSearch()  {      m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); +    m_fetch_progress.watch(m_model->activeSearchJob().get());  }  void ModrinthPage::onVersionSelectionChanged(QString version) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index b7054c88..4240dcaf 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -41,7 +41,9 @@  #include "ui/pages/BasePage.h"  #include "modplatform/modrinth/ModrinthPackManifest.h" +#include "ui/widgets/ProgressWidget.h" +#include <QTimer>  #include <QWidget>  namespace Ui { @@ -88,4 +90,9 @@ class ModrinthPage : public QWidget, public BasePage {      Modrinth::Modpack current;      QString selectedVersion; + +    ProgressWidget m_fetch_progress; + +    // Used to do instant searching with a delay to cache quick changes +    QTimer m_search_timer;  }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 6d8b2b67..78a25fea 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -10,8 +10,8 @@      <height>600</height>     </rect>    </property> -  <layout class="QVBoxLayout"> -   <item> +  <layout class="QGridLayout" name="gridLayout"> +   <item row="0" column="0">      <widget class="QLabel" name="label_2">       <property name="font">        <font> @@ -29,7 +29,7 @@       </property>      </widget>     </item> -   <item> +   <item row="1" column="0">      <layout class="QHBoxLayout">       <item>        <widget class="QLineEdit" name="searchEdit"> @@ -47,7 +47,7 @@       </item>      </layout>     </item> -   <item> +   <item row="3" column="0">      <layout class="QHBoxLayout">       <item>        <widget class="QListView" name="packView"> @@ -77,7 +77,7 @@       </item>      </layout>     </item> -   <item> +   <item row="4" column="0">      <layout class="QHBoxLayout">       <item>        <widget class="QComboBox" name="sortByBox"/> diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index e8c5ac92..3cd1d9a2 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -39,6 +39,7 @@  #include "Json.h"  #include "net/ApiDownload.h" +#include "ui/widgets/ProjectItem.h"  #include <QIcon> @@ -54,21 +55,47 @@ QVariant Technic::ListModel::data(const QModelIndex& index, int role) const      }      Modpack pack = modpacks.at(pos); -    if (role == Qt::DisplayRole) { -        return pack.name; -    } else if (role == Qt::DecorationRole) { -        if (m_logoMap.contains(pack.logoName)) { -            return (m_logoMap.value(pack.logoName)); +    switch (role) { +        case Qt::ToolTipRole: { +            if (pack.description.length() > 100) { +                // some magic to prevent to long tooltips and replace html linebreaks +                QString edit = pack.description.left(97); +                edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); +                return edit; +            } +            return pack.description; +        } +        case Qt::DecorationRole: { +            if (m_logoMap.contains(pack.logoName)) { +                return (m_logoMap.value(pack.logoName)); +            } +            QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); +            ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); +            return icon; +        } +        case Qt::UserRole: { +            QVariant v; +            v.setValue(pack); +            return v;          } -        QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); -        ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); -        return icon; -    } else if (role == Qt::UserRole) { -        QVariant v; -        v.setValue(pack); -        return v; +        case Qt::DisplayRole: +            return pack.name; +        case Qt::SizeHintRole: +            return QSize(0, 58); +        // Custom data +        case UserDataTypes::TITLE: +            return pack.name; +        case UserDataTypes::DESCRIPTION: +            return pack.description; +        case UserDataTypes::SELECTED: +            return false; +        case UserDataTypes::INSTALLED: +            return false; +        default: +            break;      } -    return QVariant(); + +    return {};  }  int Technic::ListModel::columnCount(const QModelIndex& parent) const @@ -87,21 +114,25 @@ void Technic::ListModel::searchWithTerm(const QString& term)          return;      }      currentSearchTerm = term; -    if (jobPtr) { +    if (hasActiveSearchJob()) {          jobPtr->abort();          searchState = ResetRequested;          return; -    } else { -        beginResetModel(); -        modpacks.clear(); -        endResetModel(); -        searchState = None;      } + +    beginResetModel(); +    modpacks.clear(); +    endResetModel(); +    searchState = None; +      performSearch();  }  void Technic::ListModel::performSearch()  { +    if (hasActiveSearchJob()) +        return; +      auto netJob = makeShared<NetJob>("Technic::Search", APPLICATION->network());      QString searchUrl = "";      if (currentSearchTerm.isEmpty()) { @@ -113,6 +144,9 @@ void Technic::ListModel::performSearch()      } else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) {          searchUrl = QString("%1?build=%2").arg(currentSearchTerm, BuildConfig.TECHNIC_API_BUILD);          searchMode = Single; +    } else if (currentSearchTerm.startsWith("#")) { +        searchUrl = QString("https://api.technicpack.net/modpack/%1?build=%2").arg(currentSearchTerm.mid(1), BuildConfig.TECHNIC_API_BUILD); +        searchMode = Single;      } else {          searchUrl =              QString("%1search?build=%2&q=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.h b/launcher/ui/pages/modplatform/technic/TechnicModel.h index d7a635d4..aeb4f308 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.h +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.h @@ -58,6 +58,9 @@ class ListModel : public QAbstractListModel {      void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback);      void searchWithTerm(const QString& term); +    [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } +    [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } +     private slots:      void searchRequestFinished();      void searchRequestFailed(); diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 54b86feb..190b7c68 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -34,6 +34,7 @@   */  #include "TechnicPage.h" +#include "ui/widgets/ProjectItem.h"  #include "ui_TechnicPage.h"  #include <QKeyEvent> @@ -51,7 +52,8 @@  #include "net/ApiDownload.h" -TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog) +TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget* parent) +    : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog), m_fetch_progress(this, false)  {      ui->setupUi(this);      connect(ui->searchButton, &QPushButton::clicked, this, &TechnicPage::triggerSearch); @@ -59,8 +61,21 @@ TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(p      model = new Technic::ListModel(this);      ui->packView->setModel(model); +    m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); +    m_search_timer.setSingleShot(true); + +    connect(&m_search_timer, &QTimer::timeout, this, &TechnicPage::triggerSearch); + +    m_fetch_progress.hideIfInactive(true); +    m_fetch_progress.setFixedHeight(24); +    m_fetch_progress.progressFormat(""); + +    ui->gridLayout->addWidget(&m_fetch_progress, 2, 0, 1, ui->gridLayout->columnCount()); +      connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged);      connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &TechnicPage::onVersionSelectionChanged); + +    ui->packView->setItemDelegate(new ProjectItemDelegate(this));  }  bool TechnicPage::eventFilter(QObject* watched, QEvent* event) @@ -71,6 +86,11 @@ bool TechnicPage::eventFilter(QObject* watched, QEvent* event)              triggerSearch();              keyEvent->accept();              return true; +        } else { +            if (m_search_timer.isActive()) +                m_search_timer.stop(); + +            m_search_timer.start(350);          }      }      return QWidget::eventFilter(watched, event); @@ -100,6 +120,7 @@ void TechnicPage::openedImpl()  void TechnicPage::triggerSearch()  {      model->searchWithTerm(ui->searchEdit->text()); +    m_fetch_progress.watch(model->activeSearchJob().get());  }  void TechnicPage::onSelectionChanged(QModelIndex first, [[maybe_unused]] QModelIndex second) diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index 91b61eaf..01439337 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -35,13 +35,14 @@  #pragma once +#include <QTimer>  #include <QWidget>  #include <Application.h>  #include "TechnicData.h"  #include "net/NetJob.h" -#include "tasks/Task.h"  #include "ui/pages/BasePage.h" +#include "ui/widgets/ProgressWidget.h"  namespace Ui {  class TechnicPage; @@ -91,4 +92,9 @@ class TechnicPage : public QWidget, public BasePage {      NetJob::Ptr jobPtr;      std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>(); + +    ProgressWidget m_fetch_progress; + +    // Used to do instant searching with a delay to cache quick changes +    QTimer m_search_timer;  }; diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui index 15bf645f..b988eda2 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.ui +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -11,7 +11,7 @@     </rect>    </property>    <layout class="QGridLayout" name="gridLayout"> -   <item row="3" column="0" colspan="2"> +   <item row="4" column="0" colspan="2">      <layout class="QGridLayout" name="gridLayout_3">       <item row="0" column="2">        <widget class="QComboBox" name="versionSelectionBox"/> @@ -44,7 +44,7 @@       </item>      </layout>     </item> -   <item row="2" column="0" colspan="2"> +   <item row="3" column="0" colspan="2">      <layout class="QGridLayout" name="gridLayout_2">       <item row="0" column="0">        <widget class="QListView" name="packView"> diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index 1481c1b6..60b92b28 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -34,8 +34,8 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o              icon_width = icon_size.width();              icon_height = icon_size.height(); -            icon_x_margin = (rect.height() - icon_width) / 2;              icon_y_margin = (rect.height() - icon_height) / 2; +            icon_x_margin = icon_y_margin;  // use same margins for consistency          }          // Centralize icon with a margin to separate from the other elements  | 
