aboutsummaryrefslogtreecommitdiff
path: root/launcher/pages/modplatform/ftb
diff options
context:
space:
mode:
authorPetr Mrázek <peterix@gmail.com>2021-07-25 19:11:59 +0200
committerPetr Mrázek <peterix@gmail.com>2021-07-25 19:50:44 +0200
commit20b9f2b42a3b58b6081af271774fbcc34025dccb (patch)
tree064fa59facb3357139b47bd4e60bfc8edb35ca11 /launcher/pages/modplatform/ftb
parentdd133680858351e3e07690e286882327a4f42ba5 (diff)
downloadPrismLauncher-20b9f2b42a3b58b6081af271774fbcc34025dccb.tar.gz
PrismLauncher-20b9f2b42a3b58b6081af271774fbcc34025dccb.tar.bz2
PrismLauncher-20b9f2b42a3b58b6081af271774fbcc34025dccb.zip
NOISSUE Flatten gui and logic libraries into MultiMC
Diffstat (limited to 'launcher/pages/modplatform/ftb')
-rw-r--r--launcher/pages/modplatform/ftb/FtbFilterModel.cpp64
-rw-r--r--launcher/pages/modplatform/ftb/FtbFilterModel.h33
-rw-r--r--launcher/pages/modplatform/ftb/FtbListModel.cpp304
-rw-r--r--launcher/pages/modplatform/ftb/FtbListModel.h69
-rw-r--r--launcher/pages/modplatform/ftb/FtbPage.cpp145
-rw-r--r--launcher/pages/modplatform/ftb/FtbPage.h80
-rw-r--r--launcher/pages/modplatform/ftb/FtbPage.ui84
7 files changed, 779 insertions, 0 deletions
diff --git a/launcher/pages/modplatform/ftb/FtbFilterModel.cpp b/launcher/pages/modplatform/ftb/FtbFilterModel.cpp
new file mode 100644
index 00000000..dec3a017
--- /dev/null
+++ b/launcher/pages/modplatform/ftb/FtbFilterModel.cpp
@@ -0,0 +1,64 @@
+#include "FtbFilterModel.h"
+
+#include <QDebug>
+
+#include "modplatform/modpacksch/FTBPackManifest.h"
+#include <MMCStrings.h>
+
+namespace Ftb {
+
+FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
+{
+ currentSorting = Sorting::ByPlays;
+ sortings.insert(tr("Sort by plays"), Sorting::ByPlays);
+ sortings.insert(tr("Sort by installs"), Sorting::ByInstalls);
+ sortings.insert(tr("Sort by name"), Sorting::ByName);
+}
+
+const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings()
+{
+ return sortings;
+}
+
+QString FilterModel::translateCurrentSorting()
+{
+ return sortings.key(currentSorting);
+}
+
+void FilterModel::setSorting(Sorting sorting)
+{
+ currentSorting = sorting;
+ invalidate();
+}
+
+FilterModel::Sorting FilterModel::getCurrentSorting()
+{
+ return currentSorting;
+}
+
+bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+{
+ return true;
+}
+
+bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+ ModpacksCH::Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<ModpacksCH::Modpack>();
+ ModpacksCH::Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<ModpacksCH::Modpack>();
+
+ if (currentSorting == ByPlays) {
+ return leftPack.plays < rightPack.plays;
+ }
+ else if (currentSorting == ByInstalls) {
+ return leftPack.installs < rightPack.installs;
+ }
+ else if (currentSorting == ByName) {
+ return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
+ }
+
+ // Invalid sorting set, somehow...
+ qWarning() << "Invalid sorting set!";
+ return true;
+}
+
+}
diff --git a/launcher/pages/modplatform/ftb/FtbFilterModel.h b/launcher/pages/modplatform/ftb/FtbFilterModel.h
new file mode 100644
index 00000000..4fe2a274
--- /dev/null
+++ b/launcher/pages/modplatform/ftb/FtbFilterModel.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <QtCore/QSortFilterProxyModel>
+
+namespace Ftb {
+
+class FilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ FilterModel(QObject* parent = Q_NULLPTR);
+ enum Sorting {
+ ByPlays,
+ ByInstalls,
+ ByName,
+ };
+ const QMap<QString, Sorting> getAvailableSortings();
+ QString translateCurrentSorting();
+ void setSorting(Sorting sorting);
+ Sorting getCurrentSorting();
+
+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;
+
+};
+
+}
diff --git a/launcher/pages/modplatform/ftb/FtbListModel.cpp b/launcher/pages/modplatform/ftb/FtbListModel.cpp
new file mode 100644
index 00000000..98973f2e
--- /dev/null
+++ b/launcher/pages/modplatform/ftb/FtbListModel.cpp
@@ -0,0 +1,304 @@
+#include "FtbListModel.h"
+
+#include "BuildConfig.h"
+#include "Env.h"
+#include "MultiMC.h"
+#include "Json.h"
+
+#include <QPainter>
+
+namespace Ftb {
+
+ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+ListModel::~ListModel()
+{
+}
+
+int ListModel::rowCount(const QModelIndex &parent) const
+{
+ return modpacks.size();
+}
+
+int ListModel::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+
+QVariant ListModel::data(const QModelIndex &index, int role) const
+{
+ int pos = index.row();
+ if(pos >= modpacks.size() || pos < 0 || !index.isValid())
+ {
+ return QString("INVALID INDEX %1").arg(pos);
+ }
+
+ ModpacksCH::Modpack pack = modpacks.at(pos);
+ if(role == Qt::DisplayRole)
+ {
+ return pack.name;
+ }
+ else if (role == Qt::ToolTipRole)
+ {
+ return pack.synopsis;
+ }
+ else if(role == Qt::DecorationRole)
+ {
+ QIcon placeholder = MMC->getThemedIcon("screenshot-placeholder");
+
+ auto iter = m_logoMap.find(pack.name);
+ if (iter != m_logoMap.end()) {
+ auto & logo = *iter;
+ if(!logo.result.isNull()) {
+ return logo.result;
+ }
+ return placeholder;
+ }
+
+ for(auto art : pack.art) {
+ if(art.type == "square") {
+ ((ListModel *)this)->requestLogo(pack.name, art.url);
+ }
+ }
+ return placeholder;
+ }
+ else if(role == Qt::UserRole)
+ {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+
+ return QVariant();
+}
+
+void ListModel::performSearch()
+{
+ auto *netJob = new NetJob("Ftb::Search");
+ QString searchUrl;
+ if(currentSearchTerm.isEmpty()) {
+ searchUrl = BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/all";
+ }
+ else {
+ searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/search/25?term=%1")
+ .arg(currentSearchTerm);
+ }
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ jobPtr = netJob;
+ jobPtr->start();
+ QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed);
+}
+
+void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
+{
+ if(m_logoMap.contains(logo))
+ {
+ callback(ENV.metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
+ }
+ else
+ {
+ requestLogo(logo, logoUrl);
+ }
+}
+
+void ListModel::searchWithTerm(const QString &term)
+{
+ if(searchState != Failed && currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) {
+ // unless the search has failed, then there is no need to perform an identical search.
+ return;
+ }
+ currentSearchTerm = term;
+
+ if(jobPtr) {
+ jobPtr->abort();
+ jobPtr.reset();
+ }
+
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+ searchState = None;
+
+ performSearch();
+}
+
+void ListModel::searchRequestFinished()
+{
+ jobPtr.reset();
+ remainingPacks.clear();
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ auto packs = doc.object().value("packs").toArray();
+ for(auto pack : packs) {
+ auto packId = pack.toInt();
+ remainingPacks.append(packId);
+ }
+
+ if(!remainingPacks.isEmpty()) {
+ currentPack = remainingPacks.at(0);
+ requestPack();
+ }
+}
+
+void ListModel::searchRequestFailed(QString reason)
+{
+ jobPtr.reset();
+ remainingPacks.clear();
+
+ searchState = Failed;
+}
+
+void ListModel::requestPack()
+{
+ auto *netJob = new NetJob("Ftb::Search");
+ auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1")
+ .arg(currentPack);
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ jobPtr = netJob;
+ jobPtr->start();
+
+ QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::packRequestFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &ListModel::packRequestFailed);
+}
+
+void ListModel::packRequestFinished()
+{
+ jobPtr.reset();
+ remainingPacks.removeOne(currentPack);
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ auto obj = doc.object();
+
+ ModpacksCH::Modpack pack;
+ try
+ {
+ ModpacksCH::loadModpack(pack, obj);
+ }
+ catch (const JSONValidationError &e)
+ {
+ qDebug() << QString::fromUtf8(response);
+ qWarning() << "Error while reading pack manifest from FTB: " << e.cause();
+ return;
+ }
+
+ // Since there is no guarantee that packs have a version, this will just
+ // ignore those "dud" packs.
+ if (pack.versions.empty())
+ {
+ qWarning() << "FTB Pack " << pack.id << " ignored. reason: lacking any versions";
+ }
+ else
+ {
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size());
+ modpacks.append(pack);
+ endInsertRows();
+ }
+
+ if(!remainingPacks.isEmpty()) {
+ currentPack = remainingPacks.at(0);
+ requestPack();
+ }
+}
+
+void ListModel::packRequestFailed(QString reason)
+{
+ jobPtr.reset();
+ remainingPacks.removeOne(currentPack);
+}
+
+void ListModel::logoLoaded(QString logo, bool stale)
+{
+ auto & logoObj = m_logoMap[logo];
+ logoObj.downloadJob.reset();
+ QString smallPath = logoObj.fullpath + ".small";
+
+ QFileInfo smallInfo(smallPath);
+
+ if(stale || !smallInfo.exists()) {
+ QImage image(logoObj.fullpath);
+ if (image.isNull())
+ {
+ logoObj.failed = true;
+ return;
+ }
+ QImage small;
+ if (image.width() > image.height()) {
+ small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
+ }
+ else {
+ small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
+ }
+ QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
+ QImage square(QSize(256, 256), QImage::Format_ARGB32);
+ square.fill(Qt::transparent);
+
+ QPainter painter(&square);
+ painter.drawImage(offset, small);
+ painter.end();
+
+ square.save(logoObj.fullpath + ".small", "PNG");
+ }
+
+ logoObj.result = QIcon(logoObj.fullpath + ".small");
+ for(int i = 0; i < modpacks.size(); i++) {
+ if(modpacks[i].name == logo) {
+ emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
+ }
+ }
+}
+
+void ListModel::logoFailed(QString logo)
+{
+ m_logoMap[logo].failed = true;
+ m_logoMap[logo].downloadJob.reset();
+}
+
+void ListModel::requestLogo(QString logo, QString url)
+{
+ if(m_logoMap.contains(logo)) {
+ return;
+ }
+
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)));
+
+ bool stale = entry->isStale();
+
+ NetJob *job = new NetJob(QString("FTB Icon Download %1").arg(logo));
+ job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
+
+ auto fullPath = entry->getFullPath();
+ QObject::connect(job, &NetJob::finished, this, [this, logo, fullPath, stale]
+ {
+ logoLoaded(logo, stale);
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, logo]
+ {
+ logoFailed(logo);
+ });
+
+ auto &newLogoEntry = m_logoMap[logo];
+ newLogoEntry.downloadJob = job;
+ newLogoEntry.fullpath = fullPath;
+ job->start();
+}
+
+}
diff --git a/launcher/pages/modplatform/ftb/FtbListModel.h b/launcher/pages/modplatform/ftb/FtbListModel.h
new file mode 100644
index 00000000..de94e6ba
--- /dev/null
+++ b/launcher/pages/modplatform/ftb/FtbListModel.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include <QAbstractListModel>
+
+#include "modplatform/modpacksch/FTBPackManifest.h"
+#include "net/NetJob.h"
+#include <QIcon>
+
+namespace Ftb {
+
+struct Logo {
+ QString fullpath;
+ NetJobPtr downloadJob;
+ QIcon result;
+ bool failed = false;
+};
+
+typedef QMap<QString, Logo> LogoMap;
+typedef std::function<void(QString)> LogoCallback;
+
+class ListModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ ListModel(QObject *parent);
+ virtual ~ListModel();
+
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+
+ void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
+ void searchWithTerm(const QString & term);
+
+private slots:
+ void performSearch();
+ void searchRequestFinished();
+ void searchRequestFailed(QString reason);
+
+ void requestPack();
+ void packRequestFinished();
+ void packRequestFailed(QString reason);
+
+ void logoFailed(QString logo);
+ void logoLoaded(QString logo, bool stale);
+
+private:
+ void requestLogo(QString file, QString url);
+
+private:
+ QList<ModpacksCH::Modpack> modpacks;
+ LogoMap m_logoMap;
+
+ QString currentSearchTerm;
+ enum SearchState {
+ None,
+ CanPossiblyFetchMore,
+ ResetRequested,
+ Finished,
+ Failed,
+ } searchState = None;
+ NetJobPtr jobPtr;
+ int currentPack;
+ QList<int> remainingPacks;
+ QByteArray response;
+};
+
+}
diff --git a/launcher/pages/modplatform/ftb/FtbPage.cpp b/launcher/pages/modplatform/ftb/FtbPage.cpp
new file mode 100644
index 00000000..b7f35c5d
--- /dev/null
+++ b/launcher/pages/modplatform/ftb/FtbPage.cpp
@@ -0,0 +1,145 @@
+#include "FtbPage.h"
+#include "ui_FtbPage.h"
+
+#include <QKeyEvent>
+
+#include "dialogs/NewInstanceDialog.h"
+#include "modplatform/modpacksch/FTBPackInstallTask.h"
+
+#include "HoeDown.h"
+
+FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), ui(new Ui::FtbPage), dialog(dialog)
+{
+ ui->setupUi(this);
+
+ filterModel = new Ftb::FilterModel(this);
+ listModel = new Ftb::ListModel(this);
+ filterModel->setSourceModel(listModel);
+ ui->packView->setModel(filterModel);
+ ui->packView->setSortingEnabled(true);
+ ui->packView->header()->hide();
+ ui->packView->setIndentation(0);
+
+ ui->searchEdit->installEventFilter(this);
+
+ ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
+
+ for(int i = 0; i < filterModel->getAvailableSortings().size(); i++)
+ {
+ ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i));
+ }
+ ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting());
+
+ connect(ui->searchButton, &QPushButton::clicked, this, &FtbPage::triggerSearch);
+ connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &FtbPage::onSortingSelectionChanged);
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FtbPage::onSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FtbPage::onVersionSelectionChanged);
+}
+
+FtbPage::~FtbPage()
+{
+ delete ui;
+}
+
+bool FtbPage::eventFilter(QObject* watched, QEvent* event)
+{
+ if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
+ if (keyEvent->key() == Qt::Key_Return) {
+ triggerSearch();
+ keyEvent->accept();
+ return true;
+ }
+ }
+ return QWidget::eventFilter(watched, event);
+}
+
+bool FtbPage::shouldDisplay() const
+{
+ return true;
+}
+
+void FtbPage::openedImpl()
+{
+ triggerSearch();
+ suggestCurrent();
+}
+
+void FtbPage::suggestCurrent()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+
+ if (selectedVersion.isEmpty())
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ dialog->setSuggestedPack(selected.name, new ModpacksCH::PackInstallTask(selected, selectedVersion));
+ for(auto art : selected.art) {
+ if(art.type == "square") {
+ QString editedLogoName;
+ editedLogoName = selected.name;
+
+ listModel->getLogo(selected.name, art.url, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo + ".small", editedLogoName);
+ });
+ }
+ }
+}
+
+void FtbPage::triggerSearch()
+{
+ listModel->searchWithTerm(ui->searchEdit->text());
+}
+
+void FtbPage::onSortingSelectionChanged(QString data)
+{
+ auto toSet = filterModel->getAvailableSortings().value(data);
+ filterModel->setSorting(toSet);
+}
+
+void FtbPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ ui->versionSelectionBox->clear();
+
+ if(!first.isValid())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedPack();
+ }
+ return;
+ }
+
+ selected = filterModel->data(first, Qt::UserRole).value<ModpacksCH::Modpack>();
+
+ HoeDown hoedown;
+ QString output = hoedown.process(selected.description.toUtf8());
+ ui->packDescription->setHtml(output);
+
+ // reverse foreach, so that the newest versions are first
+ for (auto i = selected.versions.size(); i--;) {
+ ui->versionSelectionBox->addItem(selected.versions.at(i).name);
+ }
+
+ suggestCurrent();
+}
+
+void FtbPage::onVersionSelectionChanged(QString data)
+{
+ if(data.isNull() || data.isEmpty())
+ {
+ selectedVersion = "";
+ return;
+ }
+
+ selectedVersion = data;
+ suggestCurrent();
+}
diff --git a/launcher/pages/modplatform/ftb/FtbPage.h b/launcher/pages/modplatform/ftb/FtbPage.h
new file mode 100644
index 00000000..c9c93897
--- /dev/null
+++ b/launcher/pages/modplatform/ftb/FtbPage.h
@@ -0,0 +1,80 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "FtbFilterModel.h"
+#include "FtbListModel.h"
+
+#include <QWidget>
+
+#include "MultiMC.h"
+#include "pages/BasePage.h"
+#include "tasks/Task.h"
+
+namespace Ui
+{
+ class FtbPage;
+}
+
+class NewInstanceDialog;
+
+class FtbPage : public QWidget, public BasePage
+{
+Q_OBJECT
+
+public:
+ explicit FtbPage(NewInstanceDialog* dialog, QWidget *parent = 0);
+ virtual ~FtbPage();
+ virtual QString displayName() const override
+ {
+ return tr("FTB");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("ftb_logo");
+ }
+ virtual QString id() const override
+ {
+ return "ftb";
+ }
+ virtual QString helpPage() const override
+ {
+ return "FTB-platform";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void openedImpl() override;
+
+ bool eventFilter(QObject * watched, QEvent * event) override;
+
+private:
+ void suggestCurrent();
+
+private slots:
+ void triggerSearch();
+ void onSortingSelectionChanged(QString data);
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
+
+private:
+ Ui::FtbPage *ui = nullptr;
+ NewInstanceDialog* dialog = nullptr;
+ Ftb::ListModel* listModel = nullptr;
+ Ftb::FilterModel* filterModel = nullptr;
+
+ ModpacksCH::Modpack selected;
+ QString selectedVersion;
+};
diff --git a/launcher/pages/modplatform/ftb/FtbPage.ui b/launcher/pages/modplatform/ftb/FtbPage.ui
new file mode 100644
index 00000000..135afc6d
--- /dev/null
+++ b/launcher/pages/modplatform/ftb/FtbPage.ui
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FtbPage</class>
+ <widget class="QWidget" name="FtbPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>875</width>
+ <height>745</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <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>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QComboBox" name="sortByBox"/>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLineEdit" name="searchEdit">
+ <property name="placeholderText">
+ <string>Search and filter ...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QPushButton" name="searchButton">
+ <property name="text">
+ <string>Search</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="0" column="0">
+ <widget class="QTreeView" name="packView">
+ <property name="iconSize">
+ <size>
+ <width>48</width>
+ <height>48</height>
+ </size>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QTextBrowser" name="packDescription">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="openLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>searchEdit</tabstop>
+ <tabstop>searchButton</tabstop>
+ <tabstop>versionSelectionBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>