Merge branch 'develop' of https://github.com/PrismLauncher/PrismLauncher into feat/launcher-updater

This commit is contained in:
Trial97 2023-09-26 20:16:46 +03:00
commit ebde563648
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
76 changed files with 1253 additions and 299 deletions

View File

@ -20,7 +20,7 @@ jobs:
if: github.repository_owner == 'PrismLauncher' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs

View File

@ -125,7 +125,7 @@ jobs:
# PREPARE
##
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
submodules: 'true'
@ -164,7 +164,7 @@ jobs:
- name: Retrieve ccache cache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
uses: actions/cache@v3.3.1
uses: actions/cache@v3.3.2
with:
path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }}
@ -620,7 +620,7 @@ jobs:
options: --privileged
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
if: inputs.build_type == 'Debug'
with:
submodules: 'true'

View File

@ -8,7 +8,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
submodules: 'true'

View File

@ -26,7 +26,7 @@ jobs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
submodules: 'true'
path: 'PrismLauncher-source'

View File

@ -16,8 +16,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v22
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@6a9a9e84a173d90b3ffb42c5ddaf9ea033fad011 # v23
- uses: DeterminateSystems/update-flake-lock@v20
with:

View File

@ -75,7 +75,6 @@ function(
set(CLANG_WARNINGS
-Wall
-Wextra # reasonable and standard
-Wextra-semi # Warn about semicolon after in-class function definition.
-Wshadow # warn the user if a variable declaration shadows one from a parent context
-Wnon-virtual-dtor # warn the user if a class with virtual functions has a non-virtual destructor. This helps
# catch hard to track down memory errors

View File

@ -21,11 +21,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1690933134,
"narHash": "sha256-ab989mN63fQZBFrkk4Q8bYxQCktuHmBIBqUG1jl6/FQ=",
"lastModified": 1693611461,
"narHash": "sha256-aPODl8vAgGQ0ZYFIRisxYG5MOGSkIczvu2Cd8Gb9+1Y=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "59cf3f1447cfc75087e7273b04b31e689a8599fb",
"rev": "7f53fdb7bdc5bb237da7fefef12d099e4fd611ca",
"type": "github"
},
"original": {
@ -89,13 +89,28 @@
"type": "github"
}
},
"nix-filter": {
"locked": {
"lastModified": 1694857738,
"narHash": "sha256-bxxNyLHjhu0N8T3REINXQ2ZkJco0ABFPn6PIe2QUfqo=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "41fd48e00c22b4ced525af521ead8792402de0ea",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-filter",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1693145325,
"narHash": "sha256-Gat9xskErH1zOcLjYMhSDBo0JTBZKfGS0xJlIRnj6Rc=",
"lastModified": 1695318763,
"narHash": "sha256-FHVPDRP2AfvsxAdc+AsgFJevMz5VBmnZglFUMlxBkcY=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "cddebdb60de376c1bdb7a4e6ee3d98355453fe56",
"rev": "e12483116b3b51a185a33a272bf351e357ba9a99",
"type": "github"
},
"original": {
@ -108,11 +123,11 @@
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1690881714,
"narHash": "sha256-h/nXluEqdiQHs1oSgkOOWF+j8gcJMWhwnZ9PFabN6q0=",
"lastModified": 1693471703,
"narHash": "sha256-0l03ZBL8P1P6z8MaSDS/MvuU8E75rVxe5eE1N6gxeTo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9e1960bc196baf6881340d53dccb203a951745a2",
"rev": "3e52e76b70d5508f3cec70b882a29199f4d1ee85",
"type": "github"
},
"original": {
@ -138,11 +153,11 @@
]
},
"locked": {
"lastModified": 1692274144,
"narHash": "sha256-BxTQuRUANQ81u8DJznQyPmRsg63t4Yc+0kcyq6OLz8s=",
"lastModified": 1694364351,
"narHash": "sha256-oadhSCqopYXxURwIA6/Anpe5IAG11q2LhvTJNP5zE6o=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "7e3517c03d46159fdbf8c0e5c97f82d5d4b0c8fa",
"rev": "4f883a76282bc28eb952570afc3d8a1bf6f481d7",
"type": "github"
},
"original": {
@ -156,6 +171,7 @@
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"libnbtplusplus": "libnbtplusplus",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks"
}

View File

@ -4,6 +4,7 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
nix-filter.url = "github:numtide/nix-filter";
pre-commit-hooks = {
url = "github:cachix/pre-commit-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs";
@ -20,8 +21,24 @@
};
};
outputs = inputs:
inputs.flake-parts.lib.mkFlake
{inherit inputs;}
{imports = [./nix];};
outputs = {
flake-parts,
pre-commit-hooks,
...
} @ inputs:
flake-parts.lib.mkFlake {inherit inputs;} {
imports = [
pre-commit-hooks.flakeModule
./nix/dev.nix
./nix/distribution.nix
];
systems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
};
}

View File

@ -311,7 +311,7 @@ QString MinecraftInstance::getLocalLibraryPath() const
bool MinecraftInstance::supportsDemo() const
{
Version instance_ver{ getPackProfile()->getComponentVersion("net.minecraft") };
// Demo mode was introduced in 1.3.1: https://minecraft.fandom.com/wiki/Demo_mode#History
// Demo mode was introduced in 1.3.1: https://minecraft.wiki/w/Demo_mode#History
// FIXME: Due to Version constraints atm, this can't handle well non-release versions
return instance_ver >= Version("1.3.1");
}

View File

@ -28,7 +28,7 @@
#include "Version.h"
// Values taken from:
// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_data_pack#%22pack_format%22
// https://minecraft.wiki/w/Tutorials/Creating_a_data_pack#%22pack_format%22
static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } },
{ 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } },

View File

@ -63,7 +63,7 @@ class DataPack : public Resource {
mutable QMutex m_data_lock;
/* The 'version' of a data pack, as defined in the pack.mcmeta file.
* See https://minecraft.fandom.com/wiki/Data_pack#pack.mcmeta
* See https://minecraft.wiki/w/Data_pack#pack.mcmeta
*/
int m_pack_format = 0;

View File

@ -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

View File

@ -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);

View File

@ -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();

View File

@ -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();

View File

@ -11,7 +11,7 @@
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
// Values taken from:
// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } },
{ 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } },
@ -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

View File

@ -51,7 +51,7 @@ class ResourcePack : public Resource {
mutable QMutex m_data_lock;
/* The 'version' of a resource pack, as defined in the pack.mcmeta file.
* See https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
* See https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
*/
int m_pack_format = 0;

View File

@ -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

View File

@ -133,7 +133,7 @@ bool processZIP(DataPack& pack, ProcessingLevel level)
return true;
}
// https://minecraft.fandom.com/wiki/Data_pack#pack.mcmeta
// https://minecraft.wiki/w/Data_pack#pack.mcmeta
bool processMCMeta(DataPack& pack, QByteArray&& raw_data)
{
try {

View File

@ -178,7 +178,7 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level)
return true;
}
// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data)
{
try {

View File

@ -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 {

View File

@ -38,6 +38,8 @@ class FlameAPI : public NetworkResourceAPI {
return 6;
case ModPlatform::ResourceType::RESOURCE_PACK:
return 12;
case ModPlatform::ResourceType::SHADER_PACK:
return 6552;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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);
}

View File

@ -61,6 +61,7 @@ class ModFolderPage : public ExternalResourcesPage {
private slots:
void removeItems(const QItemSelection& selection) override;
void deleteModMetadata();
void installMods();
void updateMods();

View File

@ -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) {

View File

@ -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();

View File

@ -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"

View File

@ -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);
}

View File

@ -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()

View File

@ -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()

View File

@ -11,44 +11,7 @@
</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>
</property>
<property name="iconSize">
<size>
<width>96</width>
<height>48</height>
</size>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QTextBrowser" name="packDescription">
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="openLinks">
<bool>true</bool>
</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">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0" colspan="2">
<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"/>
@ -68,7 +31,34 @@
</item>
</layout>
</item>
<item row="0" column="0">
<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">
<bool>true</bool>
</property>
<property name="openLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QTreeView" name="packView">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="iconSize">
<size>
<width>96</width>
<height>48</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<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>

View File

@ -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();

View File

@ -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>();
};

View File

@ -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)

View File

@ -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;
};

View File

@ -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"/>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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/>

View File

@ -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

View File

@ -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

View File

@ -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;
}
return pack.description;
} else if (role == Qt::DecorationRole) {
if (m_logoMap.contains(pack.logo)) {
return (m_logoMap.value(pack.logo));
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;
}
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::UserRole: {
QVariant v;
v.setValue(pack);
return v;
}
} else if (role == Qt::UserRole) {
QVariant v;
v.setValue(pack);
return v;
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);
}
}
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_)

View File

@ -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 {

View File

@ -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

View File

@ -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;

View File

@ -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">

View File

@ -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."));

View File

@ -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;

View File

@ -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)

View File

@ -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;
};

View File

@ -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"/>

View File

@ -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;
}
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::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;
}
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);

View File

@ -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();

View File

@ -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)

View File

@ -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;
};

View File

@ -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">

View File

@ -99,18 +99,22 @@ QDate ensureDay(int year, int month, int day)
QString JsonCatPack::path()
{
const QDate now = QDate::currentDate();
return path(QDate::currentDate());
}
QString JsonCatPack::path(QDate now)
{
for (auto var : m_variants) {
QDate startDate = ensureDay(now.year(), var.startTime.month, var.startTime.day);
QDate endDate = ensureDay(now.year(), var.endTime.month, var.endTime.day);
if (startDate > endDate) { // it's spans over multiple years
if (endDate <= now) // end date is in the past so jump one year into the future for endDate
if (endDate < now) // end date is in the past so jump one year into the future for endDate
endDate = endDate.addYears(1);
else // end date is in the future so jump one year into the past for startDate
startDate = startDate.addYears(-1);
}
if (startDate >= now && now >= endDate)
if (startDate <= now && now <= endDate)
return var.path;
}
return m_defaultPath;

View File

@ -52,9 +52,9 @@ class BasicCatPack : public CatPack {
public:
BasicCatPack(QString id, QString name) : m_id(id), m_name(name) {}
BasicCatPack(QString id) : BasicCatPack(id, id) {}
virtual QString id() { return m_id; }
virtual QString name() { return m_name; }
virtual QString path();
virtual QString id() override { return m_id; }
virtual QString name() override { return m_name; }
virtual QString path() override;
protected:
QString m_id;
@ -83,7 +83,8 @@ class JsonCatPack : public BasicCatPack {
PartialDate endTime;
};
JsonCatPack(QFileInfo& manifestInfo);
virtual QString path();
virtual QString path() override;
QString path(QDate now);
private:
QString m_defaultPath;

View File

@ -158,12 +158,12 @@ QString InfoFrame::renderColorCodes(QString input)
//
// TODO: Wrap links inside <a> tags
// https://minecraft.fandom.com/wiki/Formatting_codes#Color_codes
// https://minecraft.wiki/w/Formatting_codes#Color_codes
const QMap<QChar, QString> color_codes_map = { { '0', "#000000" }, { '1', "#0000AA" }, { '2', "#00AA00" }, { '3', "#00AAAA" },
{ '4', "#AA0000" }, { '5', "#AA00AA" }, { '6', "#FFAA00" }, { '7', "#AAAAAA" },
{ '8', "#555555" }, { '9', "#5555FF" }, { 'a', "#55FF55" }, { 'b', "#55FFFF" },
{ 'c', "#FF5555" }, { 'd', "#FF55FF" }, { 'e', "#FFFF55" }, { 'f', "#FFFFFF" } };
// https://minecraft.fandom.com/wiki/Formatting_codes#Formatting_codes
// https://minecraft.wiki/w/Formatting_codes#Formatting_codes
const QMap<QChar, QString> formatting_codes_map = { { 'l', "b" }, { 'm', "s" }, { 'n', "u" }, { 'o', "i" } };
QString html("<html>");

View File

@ -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

View File

@ -1,31 +0,0 @@
{
inputs,
self,
...
}: {
imports = [
./dev.nix
./distribution.nix
];
_module.args = {
# User-friendly version number.
version = builtins.substring 0 8 self.lastModifiedDate;
};
perSystem = {system, ...}: {
# Nixpkgs instantiated for supported systems with our overlay.
_module.args.pkgs = import inputs.nixpkgs {
inherit system;
overlays = [self.overlays.default];
};
};
# Supported systems.
systems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
}

View File

@ -1,37 +1,33 @@
{
inputs,
self,
...
}: {
perSystem = {
system,
config,
lib,
pkgs,
...
}: {
checks = {
pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run {
src = self;
hooks = {
markdownlint.enable = true;
pre-commit.settings = {
hooks = {
markdownlint.enable = true;
alejandra.enable = true;
deadnix.enable = true;
nil.enable = true;
alejandra.enable = true;
deadnix.enable = true;
nil.enable = true;
clang-format = {
enable = true;
types_or = ["c" "c++" "java" "json" "objective-c"];
};
clang-format = {
enable = true;
types_or = ["c" "c++" "java" "json" "objective-c"];
};
tools.clang-tools = pkgs.clang-tools_16;
};
tools.clang-tools = lib.mkForce pkgs.clang-tools_16;
};
devShells.default = pkgs.mkShell {
inherit (self.checks.${system}.pre-commit-check) shellHook;
shellHook = ''
${config.pre-commit.installationScript}
'';
inputsFrom = [self.packages.${system}.prismlauncher-unwrapped];
inputsFrom = [config.packages.prismlauncher-unwrapped];
buildInputs = with pkgs; [ccache ninja];
};

View File

@ -1,30 +1,65 @@
{
inputs,
self,
version,
...
}: {
perSystem = {pkgs, ...}: {
packages = {
inherit (pkgs) prismlauncher-qt5-unwrapped prismlauncher-qt5 prismlauncher-unwrapped prismlauncher;
default = pkgs.prismlauncher;
perSystem = {
lib,
pkgs,
...
}: {
packages = let
ourPackages = lib.fix (final: self.overlays.default final pkgs);
in {
inherit
(ourPackages)
prismlauncher-qt5-unwrapped
prismlauncher-qt5
prismlauncher-unwrapped
prismlauncher
;
default = ourPackages.prismlauncher;
};
};
flake = {
overlays.default = final: prev: let
# Helper function to build prism against different versions of Qt.
mkPrism = qt:
qt.callPackage ./package.nix {
inherit (inputs) libnbtplusplus;
inherit (prev.darwin.apple_sdk.frameworks) Cocoa;
inherit self version;
};
version = builtins.substring 0 8 self.lastModifiedDate or "dirty";
filteredSelf = inputs.nix-filter.lib.filter {
root = ../.;
include = [
"buildconfig"
"cmake"
"launcher"
"libraries"
"program_info"
"tests"
../COPYING.md
../CMakeLists.txt
];
};
# common args for prismlauncher evaluations
unwrappedArgs = {
self = filteredSelf;
inherit (inputs) libnbtplusplus;
inherit ((final.darwin or prev.darwin).apple_sdk.frameworks) Cocoa;
inherit version;
};
in {
prismlauncher-qt5-unwrapped = mkPrism final.libsForQt5;
prismlauncher-qt5 = prev.prismlauncher-qt5.override {prismlauncher-unwrapped = final.prismlauncher-qt5-unwrapped;};
prismlauncher-unwrapped = mkPrism final.qt6Packages;
prismlauncher = prev.prismlauncher.override {inherit (final) prismlauncher-unwrapped;};
prismlauncher-qt5-unwrapped = prev.libsForQt5.callPackage ./pkg unwrappedArgs;
prismlauncher-qt5 = prev.libsForQt5.callPackage ./pkg/wrapper.nix {
prismlauncher-unwrapped = final.prismlauncher-qt5-unwrapped;
};
prismlauncher-unwrapped = prev.qt6Packages.callPackage ./pkg unwrappedArgs;
prismlauncher = prev.qt6Packages.callPackage ./pkg/wrapper.nix {
inherit (final) prismlauncher-unwrapped;
};
};
};
}

View File

@ -58,6 +58,7 @@ assert lib.assertMsg (stdenv.isLinux || !gamemodeSupport) "gamemodeSupport is on
dontWrapQtApps = true;
meta = with lib; {
mainProgram = "prismlauncher";
homepage = "https://prismlauncher.org/";
description = "A free, open source launcher for Minecraft";
longDescription = ''

94
nix/pkg/wrapper.nix Normal file
View File

@ -0,0 +1,94 @@
{
lib,
stdenv,
symlinkJoin,
prismlauncher-unwrapped,
wrapQtAppsHook,
qtbase, # needed for wrapQtAppsHook
qtsvg,
qtwayland,
xorg,
libpulseaudio,
libGL,
glfw,
openal,
jdk8,
jdk17,
gamemode,
flite,
mesa-demos,
udev,
libusb1,
msaClientID ? null,
gamemodeSupport ? stdenv.isLinux,
textToSpeechSupport ? stdenv.isLinux,
controllerSupport ? stdenv.isLinux,
jdks ? [jdk17 jdk8],
additionalLibs ? [],
additionalPrograms ? [],
}: let
prismlauncherFinal = prismlauncher-unwrapped.override {
inherit msaClientID gamemodeSupport;
};
in
symlinkJoin {
name = "prismlauncher-${prismlauncherFinal.version}";
paths = [prismlauncherFinal];
nativeBuildInputs = [
wrapQtAppsHook
];
buildInputs =
[
qtbase
qtsvg
]
++ lib.optional (lib.versionAtLeast qtbase.version "6" && stdenv.isLinux) qtwayland;
postBuild = ''
wrapQtAppsHook
'';
qtWrapperArgs = let
runtimeLibs =
(with xorg; [
libX11
libXext
libXcursor
libXrandr
libXxf86vm
])
++ [
# lwjgl
libpulseaudio
libGL
glfw
openal
stdenv.cc.cc.lib
# oshi
udev
]
++ lib.optional gamemodeSupport gamemode.lib
++ lib.optional textToSpeechSupport flite
++ lib.optional controllerSupport libusb1
++ additionalLibs;
runtimePrograms =
[
xorg.xrandr
mesa-demos # need glxinfo
]
++ additionalPrograms;
in
["--prefix PRISMLAUNCHER_JAVA_PATHS : ${lib.makeSearchPath "bin/java" jdks}"]
++ lib.optionals stdenv.isLinux [
"--set LD_LIBRARY_PATH /run/opengl-driver/lib:${lib.makeLibraryPath runtimeLibs}"
# xorg.xrandr needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128
"--prefix PATH : ${lib.makeBinPath runtimePrograms}"
];
inherit (prismlauncherFinal) meta;
}

View File

@ -4,7 +4,7 @@ Name=Prism Launcher
Comment=A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.
Type=Application
Terminal=false
Exec=@Launcher_APP_BINARY_NAME@
Exec=@Launcher_APP_BINARY_NAME@ %U
StartupNotify=true
Icon=org.prismlauncher.PrismLauncher
Categories=Game;ActionGame;AdventureGame;Simulation;

View File

@ -56,3 +56,6 @@ ecm_add_test(Index_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}:
ecm_add_test(Version_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME Version)
ecm_add_test(CatPack_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME CatPack)

40
tests/CatPack_test.cpp Normal file
View File

@ -0,0 +1,40 @@
#include <QTest>
#include <QDate>
#include <QFileInfo>
#include <QList>
#include <QTemporaryFile>
#include "FileSystem.h"
#include "ui/themes/CatPack.h"
class CatPackTest : public QObject {
Q_OBJECT
private slots:
void test_catPack()
{
auto dataDir = QDir(QFINDTESTDATA("testdata/CatPacks")).absolutePath();
auto fileName = FS::PathCombine(dataDir, "index.json");
auto fileinfo = QFileInfo(fileName);
try {
auto cat = JsonCatPack(fileinfo);
QCOMPARE(cat.path(QDate(2023, 4, 12)), FS::PathCombine(fileinfo.path(), "oneDay.png"));
QCOMPARE(cat.path(QDate(2023, 4, 11)), FS::PathCombine(fileinfo.path(), "maxwell.png"));
QCOMPARE(cat.path(QDate(2023, 4, 13)), FS::PathCombine(fileinfo.path(), "maxwell.png"));
QCOMPARE(cat.path(QDate(2023, 12, 21)), FS::PathCombine(fileinfo.path(), "christmas.png"));
QCOMPARE(cat.path(QDate(2023, 12, 28)), FS::PathCombine(fileinfo.path(), "christmas.png"));
QCOMPARE(cat.path(QDate(2023, 12, 29)), FS::PathCombine(fileinfo.path(), "newyear.png"));
QCOMPARE(cat.path(QDate(2023, 12, 30)), FS::PathCombine(fileinfo.path(), "newyear2.png"));
QCOMPARE(cat.path(QDate(2023, 12, 31)), FS::PathCombine(fileinfo.path(), "newyear2.png"));
QCOMPARE(cat.path(QDate(2024, 1, 1)), FS::PathCombine(fileinfo.path(), "newyear2.png"));
QCOMPARE(cat.path(QDate(2024, 1, 2)), FS::PathCombine(fileinfo.path(), "newyear.png"));
QCOMPARE(cat.path(QDate(2024, 1, 3)), FS::PathCombine(fileinfo.path(), "newyear.png"));
QCOMPARE(cat.path(QDate(2024, 1, 4)), FS::PathCombine(fileinfo.path(), "maxwell.png"));
} catch (const Exception& e) {
QFAIL(e.cause().toLatin1());
}
}
};
QTEST_GUILESS_MAIN(CatPackTest)
#include "CatPack_test.moc"

50
tests/testdata/CatPacks/index.json vendored Normal file
View File

@ -0,0 +1,50 @@
{
"name": "My Cute Cat",
"default": "maxwell.png",
"variants": [
{
"startTime": {
"day": 12,
"month": 4
},
"endTime": {
"day": 12,
"month": 4
},
"path": "oneDay.png"
},
{
"startTime": {
"day": 20,
"month": 12
},
"endTime": {
"day": 28,
"month": 12
},
"path": "christmas.png"
},
{
"startTime": {
"day": 30,
"month": 12
},
"endTime": {
"day": 1,
"month": 1
},
"path": "newyear2.png"
},
{
"startTime": {
"day": 28,
"month": 12
},
"endTime": {
"day": 3,
"month": 1
},
"path": "newyear.png"
}
]
}