NOISSUE continue reshuffling the codebase

This commit is contained in:
Petr Mrázek
2021-11-22 03:55:16 +01:00
parent 5040231f8d
commit b258eac215
244 changed files with 516 additions and 768 deletions

View File

@ -0,0 +1,259 @@
#include "ListModel.h"
#include "Application.h"
#include <MMCStrings.h>
#include <Version.h>
#include <QtMath>
#include <QLabel>
#include <RWStorage.h>
#include <BuildConfig.h>
namespace LegacyFTB {
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 Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
}
//UHM, some inavlid value set?!
qWarning() << "Invalid sorting set!";
return true;
}
bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
return true;
}
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;
}
ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
{
}
ListModel::~ListModel()
{
}
QString ListModel::translatePackType(PackType type) const
{
switch(type)
{
case PackType::Public:
return tr("Public Modpack");
case PackType::ThirdParty:
return tr("Third Party Modpack");
case PackType::Private:
return tr("Private Modpack");
}
qWarning() << "Unknown FTB modpack type:" << int(type);
return QString();
}
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);
}
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;
}
return pack.description;
}
else if(role == 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;
}
else if(role == Qt::TextColorRole)
{
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;
}
return QVariant();
}
void ListModel::fill(ModpackList modpacks)
{
beginResetModel();
this->modpacks = modpacks;
endResetModel();
}
void ListModel::addPack(Modpack modpack)
{
beginResetModel();
this->modpacks.append(modpack);
endResetModel();
}
void ListModel::clear()
{
beginResetModel();
modpacks.clear();
endResetModel();
}
Modpack ListModel::at(int row)
{
return modpacks.at(row);
}
void ListModel::remove(int row)
{
if(row < 0 || row >= modpacks.size())
{
qWarning() << "Attempt to remove FTB modpacks with invalid row" << row;
return;
}
beginRemoveRows(QModelIndex(), row, row);
modpacks.removeAt(row);
endRemoveRows();
}
void ListModel::logoLoaded(QString logo, QIcon out)
{
m_loadingLogos.removeAll(logo);
m_logoMap.insert(logo, out);
emit dataChanged(createIndex(0, 0), createIndex(1, 0));
}
void ListModel::logoFailed(QString logo)
{
m_failedLogos.append(logo);
m_loadingLogos.removeAll(logo);
}
void ListModel::requestLogo(QString file)
{
if(m_loadingLogos.contains(file) || m_failedLogos.contains(file))
{
return;
}
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file.section(".", 0, 0)));
NetJob *job = new NetJob(QString("FTB Icon Download for %1").arg(file));
job->addNetAction(Net::Download::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry));
auto fullPath = entry->getFullPath();
QObject::connect(job, &NetJob::finished, this, [this, file, fullPath]
{
emit logoLoaded(file, QIcon(fullPath));
if(waitingCallbacks.contains(file))
{
waitingCallbacks.value(file)(fullPath);
}
});
QObject::connect(job, &NetJob::failed, this, [this, file]
{
emit logoFailed(file);
});
job->start(APPLICATION->network());
m_loadingLogos.append(file);
}
void ListModel::getLogo(const QString &logo, LogoCallback callback)
{
if(m_logoMap.contains(logo))
{
callback(APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
}
else
{
requestLogo(logo);
}
}
Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
{
return QAbstractListModel::flags(index);
}
}

View File

@ -0,0 +1,78 @@
#pragma once
#include <modplatform/legacy_ftb/PackHelpers.h>
#include <RWStorage.h>
#include <QAbstractListModel>
#include <QSortFilterProxyModel>
#include <QThreadPool>
#include <QIcon>
#include <QStyledItemDelegate>
#include <functional>
namespace LegacyFTB {
typedef QMap<QString, QIcon> FTBLogoMap;
typedef std::function<void(QString)> LogoCallback;
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();
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;
};
class ListModel : public QAbstractListModel
{
Q_OBJECT
private:
ModpackList modpacks;
QStringList m_failedLogos;
QStringList m_loadingLogos;
FTBLogoMap m_logoMap;
QMap<QString, LogoCallback> waitingCallbacks;
void requestLogo(QString file);
QString translatePackType(PackType type) const;
private slots:
void logoFailed(QString logo);
void logoLoaded(QString logo, QIcon out);
public:
ListModel(QObject *parent);
~ListModel();
int rowCount(const QModelIndex &parent) const override;
int columnCount(const QModelIndex &parent) const override;
QVariant data(const QModelIndex &index, int role) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
void fill(ModpackList modpacks);
void addPack(Modpack modpack);
void clear();
void remove(int row);
Modpack at(int row);
void getLogo(const QString &logo, LogoCallback callback);
};
}

View File

@ -0,0 +1,371 @@
#include "Page.h"
#include "ui_Page.h"
#include <QInputDialog>
#include "Application.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/NewInstanceDialog.h"
#include "modplatform/legacy_ftb/PackFetchTask.h"
#include "modplatform/legacy_ftb/PackInstallTask.h"
#include "modplatform/legacy_ftb/PrivatePackManager.h"
#include "ListModel.h"
namespace LegacyFTB {
Page::Page(NewInstanceDialog* dialog, QWidget *parent)
: QWidget(parent), dialog(dialog), ui(new Ui::Page)
{
ftbFetchTask.reset(new PackFetchTask(APPLICATION->network()));
ftbPrivatePacks.reset(new PrivatePackManager());
ui->setupUi(this);
{
publicFilterModel = new FilterModel(this);
publicListModel = new ListModel(this);
publicFilterModel->setSourceModel(publicListModel);
ui->publicPackList->setModel(publicFilterModel);
ui->publicPackList->setSortingEnabled(true);
ui->publicPackList->header()->hide();
ui->publicPackList->setIndentation(0);
ui->publicPackList->setIconSize(QSize(42, 42));
for(int i = 0; i < publicFilterModel->getAvailableSortings().size(); i++)
{
ui->sortByBox->addItem(publicFilterModel->getAvailableSortings().keys().at(i));
}
ui->sortByBox->setCurrentText(publicFilterModel->translateCurrentSorting());
}
{
thirdPartyFilterModel = new FilterModel(this);
thirdPartyModel = new ListModel(this);
thirdPartyFilterModel->setSourceModel(thirdPartyModel);
ui->thirdPartyPackList->setModel(thirdPartyFilterModel);
ui->thirdPartyPackList->setSortingEnabled(true);
ui->thirdPartyPackList->header()->hide();
ui->thirdPartyPackList->setIndentation(0);
ui->thirdPartyPackList->setIconSize(QSize(42, 42));
thirdPartyFilterModel->setSorting(publicFilterModel->getCurrentSorting());
}
{
privateFilterModel = new FilterModel(this);
privateListModel = new ListModel(this);
privateFilterModel->setSourceModel(privateListModel);
ui->privatePackList->setModel(privateFilterModel);
ui->privatePackList->setSortingEnabled(true);
ui->privatePackList->header()->hide();
ui->privatePackList->setIndentation(0);
ui->privatePackList->setIconSize(QSize(42, 42));
privateFilterModel->setSorting(publicFilterModel->getCurrentSorting());
}
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &Page::onSortingSelectionChanged);
connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &Page::onVersionSelectionItemChanged);
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);
connect(ui->addPackBtn, &QPushButton::pressed, this, &Page::onAddPackClicked);
connect(ui->removePackBtn, &QPushButton::pressed, this, &Page::onRemovePackClicked);
connect(ui->tabWidget, &QTabWidget::currentChanged, this, &Page::onTabChanged);
// ui->modpackInfo->setOpenExternalLinks(true);
ui->publicPackList->selectionModel()->reset();
ui->thirdPartyPackList->selectionModel()->reset();
ui->privatePackList->selectionModel()->reset();
onTabChanged(ui->tabWidget->currentIndex());
}
Page::~Page()
{
delete ui;
}
bool Page::shouldDisplay() const
{
return true;
}
void Page::openedImpl()
{
if(!initialized)
{
connect(ftbFetchTask.get(), &PackFetchTask::finished, this, &Page::ftbPackDataDownloadSuccessfully);
connect(ftbFetchTask.get(), &PackFetchTask::failed, this, &Page::ftbPackDataDownloadFailed);
connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFinished, this, &Page::ftbPrivatePackDataDownloadSuccessfully);
connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFailed, this, &Page::ftbPrivatePackDataDownloadFailed);
ftbFetchTask->fetch();
ftbPrivatePacks->load();
ftbFetchTask->fetchPrivate(ftbPrivatePacks->getCurrentPackCodes().toList());
initialized = true;
}
suggestCurrent();
}
void Page::suggestCurrent()
{
if(!isOpened)
{
return;
}
if(selected.broken || selectedVersion.isEmpty())
{
dialog->setSuggestedPack();
return;
}
dialog->setSuggestedPack(selected.name, new PackInstallTask(APPLICATION->network(), selected, selectedVersion));
QString editedLogoName;
if(selected.logo.toLower().startsWith("ftb"))
{
editedLogoName = selected.logo;
}
else
{
editedLogoName = "ftb_" + selected.logo;
}
editedLogoName = editedLogoName.left(editedLogoName.lastIndexOf(".png"));
if(selected.type == PackType::Public)
{
publicListModel->getLogo(selected.logo, [this, editedLogoName](QString logo)
{
dialog->setSuggestedIconFromFile(logo, editedLogoName);
});
}
else if (selected.type == PackType::ThirdParty)
{
thirdPartyModel->getLogo(selected.logo, [this, editedLogoName](QString logo)
{
dialog->setSuggestedIconFromFile(logo, editedLogoName);
});
}
else if (selected.type == PackType::Private)
{
privateListModel->getLogo(selected.logo, [this, editedLogoName](QString logo)
{
dialog->setSuggestedIconFromFile(logo, editedLogoName);
});
}
}
void Page::ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks)
{
publicListModel->fill(publicPacks);
thirdPartyModel->fill(thirdPartyPacks);
}
void Page::ftbPackDataDownloadFailed(QString reason)
{
//TODO: Display the error
}
void Page::ftbPrivatePackDataDownloadSuccessfully(Modpack pack)
{
privateListModel->addPack(pack);
}
void Page::ftbPrivatePackDataDownloadFailed(QString reason, QString packCode)
{
auto reply = QMessageBox::question(
this,
tr("FTB private packs"),
tr("Failed to download pack information for code %1.\nShould it be removed now?").arg(packCode)
);
if(reply == QMessageBox::Yes)
{
ftbPrivatePacks->remove(packCode);
}
}
void Page::onPublicPackSelectionChanged(QModelIndex now, QModelIndex prev)
{
if(!now.isValid())
{
onPackSelectionChanged();
return;
}
Modpack selectedPack = publicFilterModel->data(now, Qt::UserRole).value<Modpack>();
onPackSelectionChanged(&selectedPack);
}
void Page::onThirdPartyPackSelectionChanged(QModelIndex now, QModelIndex prev)
{
if(!now.isValid())
{
onPackSelectionChanged();
return;
}
Modpack selectedPack = thirdPartyFilterModel->data(now, Qt::UserRole).value<Modpack>();
onPackSelectionChanged(&selectedPack);
}
void Page::onPrivatePackSelectionChanged(QModelIndex now, QModelIndex prev)
{
if(!now.isValid())
{
onPackSelectionChanged();
return;
}
Modpack selectedPack = privateFilterModel->data(now, Qt::UserRole).value<Modpack>();
onPackSelectionChanged(&selectedPack);
}
void Page::onPackSelectionChanged(Modpack* pack)
{
ui->versionSelectionBox->clear();
if(pack)
{
currentModpackInfo->setHtml("Pack by <b>" + pack->author + "</b>" +
"<br>Minecraft " + pack->mcVersion + "<br>" + "<br>" + pack->description + "<ul><li>" + pack->mods.replace(";", "</li><li>")
+ "</li></ul>");
bool currentAdded = false;
for(int i = 0; i < pack->oldVersions.size(); i++)
{
if(pack->currentVersion == pack->oldVersions.at(i))
{
currentAdded = true;
}
ui->versionSelectionBox->addItem(pack->oldVersions.at(i));
}
if(!currentAdded)
{
ui->versionSelectionBox->addItem(pack->currentVersion);
}
selected = *pack;
}
else
{
currentModpackInfo->setHtml("");
ui->versionSelectionBox->clear();
if(isOpened)
{
dialog->setSuggestedPack();
}
return;
}
suggestCurrent();
}
void Page::onVersionSelectionItemChanged(QString data)
{
if(data.isNull() || data.isEmpty())
{
selectedVersion = "";
return;
}
selectedVersion = data;
suggestCurrent();
}
void Page::onSortingSelectionChanged(QString data)
{
FilterModel::Sorting toSet = publicFilterModel->getAvailableSortings().value(data);
publicFilterModel->setSorting(toSet);
thirdPartyFilterModel->setSorting(toSet);
privateFilterModel->setSorting(toSet);
}
void Page::onTabChanged(int tab)
{
if(tab == 1)
{
currentModel = thirdPartyFilterModel;
currentList = ui->thirdPartyPackList;
currentModpackInfo = ui->thirdPartyPackDescription;
}
else if(tab == 2)
{
currentModel = privateFilterModel;
currentList = ui->privatePackList;
currentModpackInfo = ui->privatePackDescription;
}
else
{
currentModel = publicFilterModel;
currentList = ui->publicPackList;
currentModpackInfo = ui->publicPackDescription;
}
currentList->selectionModel()->reset();
QModelIndex idx = currentList->currentIndex();
if(idx.isValid())
{
auto pack = currentModel->data(idx, Qt::UserRole).value<Modpack>();
onPackSelectionChanged(&pack);
}
else
{
onPackSelectionChanged();
}
}
void Page::onAddPackClicked()
{
bool ok;
QString text = QInputDialog::getText(
this,
tr("Add FTB pack"),
tr("Enter pack code:"),
QLineEdit::Normal,
QString(),
&ok
);
if(ok && !text.isEmpty())
{
ftbPrivatePacks->add(text);
ftbFetchTask->fetchPrivate({text});
}
}
void Page::onRemovePackClicked()
{
auto index = ui->privatePackList->currentIndex();
if(!index.isValid())
{
return;
}
auto row = index.row();
Modpack pack = privateListModel->at(row);
auto answer = QMessageBox::question(
this,
tr("Remove pack"),
tr("Are you sure you want to remove pack %1?").arg(pack.name),
QMessageBox::Yes | QMessageBox::No
);
if(answer != QMessageBox::Yes)
{
return;
}
ftbPrivatePacks->remove(pack.packCode);
privateListModel->remove(row);
onPackSelectionChanged();
}
}

View File

@ -0,0 +1,119 @@
/* 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 <QWidget>
#include <QTreeView>
#include <QTextBrowser>
#include "ui/pages/BasePage.h"
#include <Application.h>
#include "tasks/Task.h"
#include "modplatform/legacy_ftb/PackHelpers.h"
#include "modplatform/legacy_ftb/PackFetchTask.h"
#include "QObjectPtr.h"
class NewInstanceDialog;
namespace LegacyFTB {
namespace Ui
{
class Page;
}
class ListModel;
class FilterModel;
class PrivatePackListModel;
class PrivatePackFilterModel;
class PrivatePackManager;
class Page : public QWidget, public BasePage
{
Q_OBJECT
public:
explicit Page(NewInstanceDialog * dialog, QWidget *parent = 0);
virtual ~Page();
QString displayName() const override
{
return tr("FTB Legacy");
}
QIcon icon() const override
{
return APPLICATION->getThemedIcon("ftb_logo");
}
QString id() const override
{
return "legacy_ftb";
}
QString helpPage() const override
{
return "FTB-platform";
}
bool shouldDisplay() const override;
void openedImpl() override;
private:
void suggestCurrent();
void onPackSelectionChanged(Modpack *pack = nullptr);
private slots:
void ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks);
void ftbPackDataDownloadFailed(QString reason);
void ftbPrivatePackDataDownloadSuccessfully(Modpack pack);
void ftbPrivatePackDataDownloadFailed(QString reason, QString packCode);
void onSortingSelectionChanged(QString data);
void onVersionSelectionItemChanged(QString data);
void onPublicPackSelectionChanged(QModelIndex first, QModelIndex second);
void onThirdPartyPackSelectionChanged(QModelIndex first, QModelIndex second);
void onPrivatePackSelectionChanged(QModelIndex first, QModelIndex second);
void onTabChanged(int tab);
void onAddPackClicked();
void onRemovePackClicked();
private:
FilterModel* currentModel = nullptr;
QTreeView* currentList = nullptr;
QTextBrowser* currentModpackInfo = nullptr;
bool initialized = false;
Modpack selected;
QString selectedVersion;
ListModel* publicListModel = nullptr;
FilterModel* publicFilterModel = nullptr;
ListModel *thirdPartyModel = nullptr;
FilterModel *thirdPartyFilterModel = nullptr;
ListModel *privateListModel = nullptr;
FilterModel *privateFilterModel = nullptr;
unique_qobject_ptr<PackFetchTask> ftbFetchTask;
std::unique_ptr<PrivatePackManager> ftbPrivatePacks;
NewInstanceDialog* dialog = nullptr;
Ui::Page *ui = nullptr;
};
}

View File

@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LegacyFTB::Page</class>
<widget class="QWidget" name="LegacyFTB::Page">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>709</width>
<height>602</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Public</string>
</attribute>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTreeView" name="publicPackList">
<property name="maximumSize">
<size>
<width>250</width>
<height>16777215</height>
</size>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QTextBrowser" name="publicPackDescription"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>3rd Party</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="1">
<widget class="QTextBrowser" name="thirdPartyPackDescription"/>
</item>
<item row="0" column="0">
<widget class="QTreeView" name="thirdPartyPackList">
<property name="maximumSize">
<size>
<width>250</width>
<height>16777215</height>
</size>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_3">
<attribute name="title">
<string>Private</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QTreeView" name="privatePackList">
<property name="maximumSize">
<size>
<width>250</width>
<height>16777215</height>
</size>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="addPackBtn">
<property name="text">
<string>Add pack</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="removePackBtn">
<property name="text">
<string>Remove selected pack</string>
</property>
</widget>
</item>
<item row="0" column="1" rowspan="3">
<widget class="QTextBrowser" name="privatePackDescription"/>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_4">
<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="2">
<widget class="QComboBox" name="versionSelectionBox"/>
</item>
<item row="0" column="0">
<widget class="QComboBox" name="sortByBox">
<property name="minimumSize">
<size>
<width>265</width>
<height>0</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>