mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-02-09 13:03:09 +00:00
1401 lines
40 KiB
C++
1401 lines
40 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
|
|
|
#include "app.h"
|
|
#include "bottlesimporter.h"
|
|
#include "desktopimporter.h"
|
|
#include "flatpakimporter.h"
|
|
#include "heroicimporter.h"
|
|
#include "itchimporter.h"
|
|
#include "legendaryimporter.h"
|
|
#include "lutrisimporter.h"
|
|
#include "retroarchimporter.h"
|
|
#include "steamimporter.h"
|
|
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonParseError>
|
|
#include <QSet>
|
|
#include <QStandardPaths>
|
|
#include <QTimer>
|
|
#include <QtConcurrent>
|
|
|
|
App *App::s_instance = nullptr;
|
|
|
|
App::App(QObject *parent)
|
|
: QObject(parent)
|
|
, m_gameModel(new GameModel(this))
|
|
, m_launcher(new GameLauncher(this))
|
|
, m_steamGridDB(new SteamGridDB(this))
|
|
, m_mediaManager(new MediaManager(this))
|
|
, m_config(new Config(this))
|
|
{
|
|
loadLibrary();
|
|
|
|
if (!m_config->importSteam()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("Steam")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
if (!m_config->importLutris()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("Lutris")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
if (!m_config->importHeroic()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("Heroic")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
if (!m_config->importDesktop()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("Desktop")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
if (!m_config->importBottles()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("Bottles")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
if (!m_config->importFlatpak()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("Flatpak")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
if (!m_config->importItch()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("itch.io")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
if (!m_config->importLegendary()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("Legendary")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
if (!m_config->importRetroArch()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("RetroArch")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
|
|
connect(m_config, &Config::importSteamChanged, this, [this]() {
|
|
if (!m_config->importSteam()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("Steam")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
});
|
|
connect(m_config, &Config::importLutrisChanged, this, [this]() {
|
|
if (!m_config->importLutris()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("Lutris")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
});
|
|
connect(m_config, &Config::importHeroicChanged, this, [this]() {
|
|
if (!m_config->importHeroic()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("Heroic")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
});
|
|
connect(m_config, &Config::importDesktopChanged, this, [this]() {
|
|
if (!m_config->importDesktop()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("Desktop")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
});
|
|
connect(m_config, &Config::importBottlesChanged, this, [this]() {
|
|
if (!m_config->importBottles()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("Bottles")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
});
|
|
connect(m_config, &Config::importFlatpakChanged, this, [this]() {
|
|
if (!m_config->importFlatpak()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("Flatpak")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
});
|
|
connect(m_config, &Config::importItchChanged, this, [this]() {
|
|
if (!m_config->importItch()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("itch.io")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
});
|
|
connect(m_config, &Config::importLegendaryChanged, this, [this]() {
|
|
if (!m_config->importLegendary()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("Legendary")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
});
|
|
connect(m_config, &Config::importRetroArchChanged, this, [this]() {
|
|
if (!m_config->importRetroArch()) {
|
|
if (m_gameModel->removeByPlatformPrefix(QStringLiteral("RetroArch")) > 0) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
});
|
|
|
|
if (m_config->autoImportOnStartup()) {
|
|
QTimer::singleShot(0, this, [this]() {
|
|
importAllGames();
|
|
});
|
|
}
|
|
}
|
|
|
|
App *App::instance()
|
|
{
|
|
if (!s_instance) {
|
|
s_instance = new App();
|
|
}
|
|
return s_instance;
|
|
}
|
|
|
|
App *App::create(QQmlEngine *engine, QJSEngine *scriptEngine)
|
|
{
|
|
Q_UNUSED(engine)
|
|
Q_UNUSED(scriptEngine)
|
|
return instance();
|
|
}
|
|
|
|
GameModel *App::gameModel() const
|
|
{
|
|
return m_gameModel;
|
|
}
|
|
|
|
GameLauncher *App::launcher() const
|
|
{
|
|
return m_launcher;
|
|
}
|
|
|
|
SteamGridDB *App::steamGridDB() const
|
|
{
|
|
return m_steamGridDB;
|
|
}
|
|
|
|
MediaManager *App::mediaManager() const
|
|
{
|
|
return m_mediaManager;
|
|
}
|
|
|
|
Config *App::config() const
|
|
{
|
|
return m_config;
|
|
}
|
|
|
|
bool App::importing() const
|
|
{
|
|
return m_importing;
|
|
}
|
|
|
|
QString App::importStatus() const
|
|
{
|
|
return m_importStatus;
|
|
}
|
|
|
|
void App::setImporting(bool importing)
|
|
{
|
|
if (m_importing != importing) {
|
|
m_importing = importing;
|
|
Q_EMIT importingChanged();
|
|
}
|
|
}
|
|
|
|
void App::setImportStatus(const QString &status)
|
|
{
|
|
if (m_importStatus != status) {
|
|
m_importStatus = status;
|
|
Q_EMIT importStatusChanged();
|
|
}
|
|
}
|
|
|
|
void App::importAllGames()
|
|
{
|
|
if (m_importing)
|
|
return;
|
|
|
|
const bool doSteam = m_config->importSteam();
|
|
const bool doLutris = m_config->importLutris();
|
|
const bool doHeroic = m_config->importHeroic();
|
|
const bool doDesktop = m_config->importDesktop();
|
|
const bool doBottles = m_config->importBottles();
|
|
const bool doFlatpak = m_config->importFlatpak();
|
|
const bool doItch = m_config->importItch();
|
|
const bool doLegendary = m_config->importLegendary();
|
|
const bool doRetroArch = m_config->importRetroArch();
|
|
|
|
const bool anyEnabled = doSteam || doLutris || doHeroic || doDesktop || doBottles || doFlatpak || doItch || doLegendary || doRetroArch;
|
|
|
|
if (!anyEnabled) {
|
|
setImportStatus(tr("No import sources enabled"));
|
|
Q_EMIT importCompleted(0);
|
|
return;
|
|
}
|
|
|
|
const QSet<QString> existingIds = [this]() {
|
|
QSet<QString> ids;
|
|
const QList<Game *> games = m_gameModel->allGames();
|
|
for (Game *game : games) {
|
|
if (game) {
|
|
ids.insert(game->id());
|
|
}
|
|
}
|
|
return ids;
|
|
}();
|
|
|
|
setImporting(true);
|
|
setImportStatus(tr("Importing games..."));
|
|
|
|
[[maybe_unused]] auto future = QtConcurrent::run([this,
|
|
doSteam,
|
|
doLutris,
|
|
doHeroic,
|
|
doDesktop,
|
|
doBottles,
|
|
doFlatpak,
|
|
doItch,
|
|
doLegendary,
|
|
doRetroArch,
|
|
existingIds]() {
|
|
int totalCount = 0;
|
|
QSet<QString> seenIds = existingIds;
|
|
|
|
const auto keepNewGames = [&seenIds](const QList<Game *> &games) {
|
|
QList<Game *> result;
|
|
result.reserve(games.size());
|
|
|
|
for (Game *game : games) {
|
|
if (!game) {
|
|
continue;
|
|
}
|
|
|
|
const QString id = game->id();
|
|
if (id.isEmpty() || seenIds.contains(id)) {
|
|
delete game;
|
|
continue;
|
|
}
|
|
|
|
seenIds.insert(id);
|
|
result.append(game);
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
// Import from Steam
|
|
if (doSteam) {
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this]() {
|
|
setImportStatus(tr("Scanning Steam library..."));
|
|
},
|
|
Qt::QueuedConnection);
|
|
|
|
SteamImporter steamImporter;
|
|
QList<Game *> steamGames = keepNewGames(steamImporter.importGames());
|
|
for (Game *game : steamGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, steamGames]() {
|
|
if (!m_config->importSteam()) {
|
|
for (Game *game : steamGames) {
|
|
if (game) {
|
|
game->deleteLater();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
for (Game *game : steamGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
},
|
|
Qt::QueuedConnection);
|
|
totalCount += steamGames.count();
|
|
}
|
|
|
|
// Import from Lutris
|
|
if (doLutris) {
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this]() {
|
|
setImportStatus(tr("Scanning Lutris library..."));
|
|
},
|
|
Qt::QueuedConnection);
|
|
|
|
LutrisImporter lutrisImporter;
|
|
QList<Game *> lutrisGames = keepNewGames(lutrisImporter.importGames());
|
|
for (Game *game : lutrisGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, lutrisGames]() {
|
|
if (!m_config->importLutris()) {
|
|
for (Game *game : lutrisGames) {
|
|
if (game) {
|
|
game->deleteLater();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
for (Game *game : lutrisGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
},
|
|
Qt::QueuedConnection);
|
|
totalCount += lutrisGames.count();
|
|
}
|
|
|
|
// Import from Heroic
|
|
if (doHeroic) {
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this]() {
|
|
setImportStatus(tr("Scanning Heroic library..."));
|
|
},
|
|
Qt::QueuedConnection);
|
|
|
|
HeroicImporter heroicImporter;
|
|
QList<Game *> heroicGames = keepNewGames(heroicImporter.importGames());
|
|
for (Game *game : heroicGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, heroicGames]() {
|
|
if (!m_config->importHeroic()) {
|
|
for (Game *game : heroicGames) {
|
|
if (game) {
|
|
game->deleteLater();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
for (Game *game : heroicGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
},
|
|
Qt::QueuedConnection);
|
|
totalCount += heroicGames.count();
|
|
}
|
|
|
|
// Import from Desktop entries
|
|
if (doDesktop) {
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this]() {
|
|
setImportStatus(tr("Scanning desktop entries..."));
|
|
},
|
|
Qt::QueuedConnection);
|
|
|
|
DesktopImporter desktopImporter;
|
|
QList<Game *> desktopGames = keepNewGames(desktopImporter.importGames());
|
|
for (Game *game : desktopGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, desktopGames]() {
|
|
if (!m_config->importDesktop()) {
|
|
for (Game *game : desktopGames) {
|
|
if (game) {
|
|
game->deleteLater();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
for (Game *game : desktopGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
},
|
|
Qt::QueuedConnection);
|
|
totalCount += desktopGames.count();
|
|
}
|
|
|
|
// Import from Bottles
|
|
if (doBottles) {
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this]() {
|
|
setImportStatus(tr("Scanning Bottles..."));
|
|
},
|
|
Qt::QueuedConnection);
|
|
|
|
BottlesImporter bottlesImporter;
|
|
QList<Game *> bottlesGames = keepNewGames(bottlesImporter.importGames());
|
|
for (Game *game : bottlesGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, bottlesGames]() {
|
|
if (!m_config->importBottles()) {
|
|
for (Game *game : bottlesGames) {
|
|
if (game) {
|
|
game->deleteLater();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
for (Game *game : bottlesGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
},
|
|
Qt::QueuedConnection);
|
|
totalCount += bottlesGames.count();
|
|
}
|
|
|
|
// Import from Flatpak
|
|
if (doFlatpak) {
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this]() {
|
|
setImportStatus(tr("Scanning Flatpak games..."));
|
|
},
|
|
Qt::QueuedConnection);
|
|
|
|
FlatpakImporter flatpakImporter;
|
|
QList<Game *> flatpakGames = keepNewGames(flatpakImporter.importGames());
|
|
for (Game *game : flatpakGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, flatpakGames]() {
|
|
if (!m_config->importFlatpak()) {
|
|
for (Game *game : flatpakGames) {
|
|
if (game) {
|
|
game->deleteLater();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
for (Game *game : flatpakGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
},
|
|
Qt::QueuedConnection);
|
|
totalCount += flatpakGames.count();
|
|
}
|
|
|
|
// Import from itch.io
|
|
if (doItch) {
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this]() {
|
|
setImportStatus(tr("Scanning itch.io library..."));
|
|
},
|
|
Qt::QueuedConnection);
|
|
|
|
ItchImporter itchImporter;
|
|
QList<Game *> itchGames = keepNewGames(itchImporter.importGames());
|
|
for (Game *game : itchGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, itchGames]() {
|
|
if (!m_config->importItch()) {
|
|
for (Game *game : itchGames) {
|
|
if (game) {
|
|
game->deleteLater();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
for (Game *game : itchGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
},
|
|
Qt::QueuedConnection);
|
|
totalCount += itchGames.count();
|
|
}
|
|
|
|
// Import from Legendary
|
|
if (doLegendary) {
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this]() {
|
|
setImportStatus(tr("Scanning Legendary library..."));
|
|
},
|
|
Qt::QueuedConnection);
|
|
|
|
LegendaryImporter legendaryImporter;
|
|
QList<Game *> legendaryGames = keepNewGames(legendaryImporter.importGames());
|
|
for (Game *game : legendaryGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, legendaryGames]() {
|
|
if (!m_config->importLegendary()) {
|
|
for (Game *game : legendaryGames) {
|
|
if (game) {
|
|
game->deleteLater();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
for (Game *game : legendaryGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
},
|
|
Qt::QueuedConnection);
|
|
totalCount += legendaryGames.count();
|
|
}
|
|
|
|
// Import from RetroArch
|
|
if (doRetroArch) {
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this]() {
|
|
setImportStatus(tr("Scanning RetroArch playlists..."));
|
|
},
|
|
Qt::QueuedConnection);
|
|
|
|
RetroArchImporter retroArchImporter;
|
|
QList<Game *> retroArchGames = keepNewGames(retroArchImporter.importGames());
|
|
for (Game *game : retroArchGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, retroArchGames]() {
|
|
if (!m_config->importRetroArch()) {
|
|
for (Game *game : retroArchGames) {
|
|
if (game) {
|
|
game->deleteLater();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
for (Game *game : retroArchGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
},
|
|
Qt::QueuedConnection);
|
|
totalCount += retroArchGames.count();
|
|
}
|
|
|
|
// Complete
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, totalCount]() {
|
|
setImportStatus(tr("Import complete: %1 new games found").arg(totalCount));
|
|
setImporting(false);
|
|
saveLibrary();
|
|
Q_EMIT importCompleted(totalCount);
|
|
},
|
|
Qt::QueuedConnection);
|
|
});
|
|
}
|
|
|
|
void App::importFromSteam()
|
|
{
|
|
if (m_importing)
|
|
return;
|
|
|
|
const QSet<QString> existingIds = [this]() {
|
|
QSet<QString> ids;
|
|
const QList<Game *> games = m_gameModel->allGames();
|
|
for (Game *game : games) {
|
|
if (game) {
|
|
ids.insert(game->id());
|
|
}
|
|
}
|
|
return ids;
|
|
}();
|
|
|
|
setImporting(true);
|
|
setImportStatus(tr("Scanning Steam library..."));
|
|
|
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
|
SteamImporter importer;
|
|
QList<Game *> games = importer.importGames();
|
|
QSet<QString> seenIds = existingIds;
|
|
QList<Game *> newGames;
|
|
newGames.reserve(games.size());
|
|
|
|
for (Game *game : games) {
|
|
if (!game) {
|
|
continue;
|
|
}
|
|
|
|
const QString id = game->id();
|
|
if (id.isEmpty() || seenIds.contains(id)) {
|
|
delete game;
|
|
continue;
|
|
}
|
|
|
|
seenIds.insert(id);
|
|
newGames.append(game);
|
|
}
|
|
|
|
for (Game *game : newGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, newGames]() {
|
|
for (Game *game : newGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
setImportStatus(tr("Steam import complete: %1 new games found").arg(newGames.count()));
|
|
setImporting(false);
|
|
saveLibrary();
|
|
Q_EMIT importCompleted(newGames.count());
|
|
},
|
|
Qt::QueuedConnection);
|
|
});
|
|
}
|
|
|
|
void App::importFromLutris()
|
|
{
|
|
if (m_importing)
|
|
return;
|
|
|
|
const QSet<QString> existingIds = [this]() {
|
|
QSet<QString> ids;
|
|
const QList<Game *> games = m_gameModel->allGames();
|
|
for (Game *game : games) {
|
|
if (game) {
|
|
ids.insert(game->id());
|
|
}
|
|
}
|
|
return ids;
|
|
}();
|
|
|
|
setImporting(true);
|
|
setImportStatus(tr("Scanning Lutris library..."));
|
|
|
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
|
LutrisImporter importer;
|
|
QList<Game *> games = importer.importGames();
|
|
QSet<QString> seenIds = existingIds;
|
|
QList<Game *> newGames;
|
|
newGames.reserve(games.size());
|
|
|
|
for (Game *game : games) {
|
|
if (!game) {
|
|
continue;
|
|
}
|
|
|
|
const QString id = game->id();
|
|
if (id.isEmpty() || seenIds.contains(id)) {
|
|
delete game;
|
|
continue;
|
|
}
|
|
|
|
seenIds.insert(id);
|
|
newGames.append(game);
|
|
}
|
|
|
|
for (Game *game : newGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, newGames]() {
|
|
for (Game *game : newGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
setImportStatus(tr("Lutris import complete: %1 new games found").arg(newGames.count()));
|
|
setImporting(false);
|
|
saveLibrary();
|
|
Q_EMIT importCompleted(newGames.count());
|
|
},
|
|
Qt::QueuedConnection);
|
|
});
|
|
}
|
|
|
|
void App::importFromHeroic()
|
|
{
|
|
if (m_importing)
|
|
return;
|
|
|
|
const QSet<QString> existingIds = [this]() {
|
|
QSet<QString> ids;
|
|
const QList<Game *> games = m_gameModel->allGames();
|
|
for (Game *game : games) {
|
|
if (game) {
|
|
ids.insert(game->id());
|
|
}
|
|
}
|
|
return ids;
|
|
}();
|
|
|
|
setImporting(true);
|
|
setImportStatus(tr("Scanning Heroic library..."));
|
|
|
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
|
HeroicImporter importer;
|
|
QList<Game *> games = importer.importGames();
|
|
QSet<QString> seenIds = existingIds;
|
|
QList<Game *> newGames;
|
|
newGames.reserve(games.size());
|
|
|
|
for (Game *game : games) {
|
|
if (!game) {
|
|
continue;
|
|
}
|
|
|
|
const QString id = game->id();
|
|
if (id.isEmpty() || seenIds.contains(id)) {
|
|
delete game;
|
|
continue;
|
|
}
|
|
|
|
seenIds.insert(id);
|
|
newGames.append(game);
|
|
}
|
|
|
|
for (Game *game : newGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, newGames]() {
|
|
for (Game *game : newGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
setImportStatus(tr("Heroic import complete: %1 new games found").arg(newGames.count()));
|
|
setImporting(false);
|
|
saveLibrary();
|
|
Q_EMIT importCompleted(newGames.count());
|
|
},
|
|
Qt::QueuedConnection);
|
|
});
|
|
}
|
|
|
|
void App::importFromDesktop()
|
|
{
|
|
if (m_importing)
|
|
return;
|
|
|
|
const QSet<QString> existingIds = [this]() {
|
|
QSet<QString> ids;
|
|
const QList<Game *> games = m_gameModel->allGames();
|
|
for (Game *game : games) {
|
|
if (game) {
|
|
ids.insert(game->id());
|
|
}
|
|
}
|
|
return ids;
|
|
}();
|
|
|
|
setImporting(true);
|
|
setImportStatus(tr("Scanning desktop entries..."));
|
|
|
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
|
DesktopImporter importer;
|
|
QList<Game *> games = importer.importGames();
|
|
QSet<QString> seenIds = existingIds;
|
|
QList<Game *> newGames;
|
|
newGames.reserve(games.size());
|
|
|
|
for (Game *game : games) {
|
|
if (!game) {
|
|
continue;
|
|
}
|
|
|
|
const QString id = game->id();
|
|
if (id.isEmpty() || seenIds.contains(id)) {
|
|
delete game;
|
|
continue;
|
|
}
|
|
|
|
seenIds.insert(id);
|
|
newGames.append(game);
|
|
}
|
|
|
|
for (Game *game : newGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, newGames]() {
|
|
for (Game *game : newGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
setImportStatus(tr("Desktop import complete: %1 new games found").arg(newGames.count()));
|
|
setImporting(false);
|
|
saveLibrary();
|
|
Q_EMIT importCompleted(newGames.count());
|
|
},
|
|
Qt::QueuedConnection);
|
|
});
|
|
}
|
|
|
|
void App::importFromBottles()
|
|
{
|
|
if (m_importing)
|
|
return;
|
|
|
|
const QSet<QString> existingIds = [this]() {
|
|
QSet<QString> ids;
|
|
const QList<Game *> games = m_gameModel->allGames();
|
|
for (Game *game : games) {
|
|
if (game) {
|
|
ids.insert(game->id());
|
|
}
|
|
}
|
|
return ids;
|
|
}();
|
|
|
|
setImporting(true);
|
|
setImportStatus(tr("Scanning Bottles..."));
|
|
|
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
|
BottlesImporter importer;
|
|
QList<Game *> games = importer.importGames();
|
|
QSet<QString> seenIds = existingIds;
|
|
QList<Game *> newGames;
|
|
newGames.reserve(games.size());
|
|
|
|
for (Game *game : games) {
|
|
if (!game) {
|
|
continue;
|
|
}
|
|
|
|
const QString id = game->id();
|
|
if (id.isEmpty() || seenIds.contains(id)) {
|
|
delete game;
|
|
continue;
|
|
}
|
|
|
|
seenIds.insert(id);
|
|
newGames.append(game);
|
|
}
|
|
|
|
for (Game *game : newGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, newGames]() {
|
|
for (Game *game : newGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
setImportStatus(tr("Bottles import complete: %1 new games found").arg(newGames.count()));
|
|
setImporting(false);
|
|
saveLibrary();
|
|
Q_EMIT importCompleted(newGames.count());
|
|
},
|
|
Qt::QueuedConnection);
|
|
});
|
|
}
|
|
|
|
void App::importFromFlatpak()
|
|
{
|
|
if (m_importing)
|
|
return;
|
|
|
|
const QSet<QString> existingIds = [this]() {
|
|
QSet<QString> ids;
|
|
const QList<Game *> games = m_gameModel->allGames();
|
|
for (Game *game : games) {
|
|
if (game) {
|
|
ids.insert(game->id());
|
|
}
|
|
}
|
|
return ids;
|
|
}();
|
|
|
|
setImporting(true);
|
|
setImportStatus(tr("Scanning Flatpak games..."));
|
|
|
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
|
FlatpakImporter importer;
|
|
QList<Game *> games = importer.importGames();
|
|
QSet<QString> seenIds = existingIds;
|
|
QList<Game *> newGames;
|
|
newGames.reserve(games.size());
|
|
|
|
for (Game *game : games) {
|
|
if (!game) {
|
|
continue;
|
|
}
|
|
|
|
const QString id = game->id();
|
|
if (id.isEmpty() || seenIds.contains(id)) {
|
|
delete game;
|
|
continue;
|
|
}
|
|
|
|
seenIds.insert(id);
|
|
newGames.append(game);
|
|
}
|
|
|
|
for (Game *game : newGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, newGames]() {
|
|
for (Game *game : newGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
setImportStatus(tr("Flatpak import complete: %1 new games found").arg(newGames.count()));
|
|
setImporting(false);
|
|
saveLibrary();
|
|
Q_EMIT importCompleted(newGames.count());
|
|
},
|
|
Qt::QueuedConnection);
|
|
});
|
|
}
|
|
|
|
void App::importFromItch()
|
|
{
|
|
if (m_importing)
|
|
return;
|
|
|
|
const QSet<QString> existingIds = [this]() {
|
|
QSet<QString> ids;
|
|
const QList<Game *> games = m_gameModel->allGames();
|
|
for (Game *game : games) {
|
|
if (game) {
|
|
ids.insert(game->id());
|
|
}
|
|
}
|
|
return ids;
|
|
}();
|
|
|
|
setImporting(true);
|
|
setImportStatus(tr("Scanning itch.io library..."));
|
|
|
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
|
ItchImporter importer;
|
|
QList<Game *> games = importer.importGames();
|
|
QSet<QString> seenIds = existingIds;
|
|
QList<Game *> newGames;
|
|
newGames.reserve(games.size());
|
|
|
|
for (Game *game : games) {
|
|
if (!game) {
|
|
continue;
|
|
}
|
|
|
|
const QString id = game->id();
|
|
if (id.isEmpty() || seenIds.contains(id)) {
|
|
delete game;
|
|
continue;
|
|
}
|
|
|
|
seenIds.insert(id);
|
|
newGames.append(game);
|
|
}
|
|
|
|
for (Game *game : newGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, newGames]() {
|
|
for (Game *game : newGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
setImportStatus(tr("itch.io import complete: %1 new games found").arg(newGames.count()));
|
|
setImporting(false);
|
|
saveLibrary();
|
|
Q_EMIT importCompleted(newGames.count());
|
|
},
|
|
Qt::QueuedConnection);
|
|
});
|
|
}
|
|
|
|
void App::importFromLegendary()
|
|
{
|
|
if (m_importing)
|
|
return;
|
|
|
|
const QSet<QString> existingIds = [this]() {
|
|
QSet<QString> ids;
|
|
const QList<Game *> games = m_gameModel->allGames();
|
|
for (Game *game : games) {
|
|
if (game) {
|
|
ids.insert(game->id());
|
|
}
|
|
}
|
|
return ids;
|
|
}();
|
|
|
|
setImporting(true);
|
|
setImportStatus(tr("Scanning Legendary library..."));
|
|
|
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
|
LegendaryImporter importer;
|
|
QList<Game *> games = importer.importGames();
|
|
QSet<QString> seenIds = existingIds;
|
|
QList<Game *> newGames;
|
|
newGames.reserve(games.size());
|
|
|
|
for (Game *game : games) {
|
|
if (!game) {
|
|
continue;
|
|
}
|
|
|
|
const QString id = game->id();
|
|
if (id.isEmpty() || seenIds.contains(id)) {
|
|
delete game;
|
|
continue;
|
|
}
|
|
|
|
seenIds.insert(id);
|
|
newGames.append(game);
|
|
}
|
|
|
|
for (Game *game : newGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, newGames]() {
|
|
for (Game *game : newGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
setImportStatus(tr("Legendary import complete: %1 new games found").arg(newGames.count()));
|
|
setImporting(false);
|
|
saveLibrary();
|
|
Q_EMIT importCompleted(newGames.count());
|
|
},
|
|
Qt::QueuedConnection);
|
|
});
|
|
}
|
|
|
|
void App::importFromRetroArch()
|
|
{
|
|
if (m_importing)
|
|
return;
|
|
|
|
const QSet<QString> existingIds = [this]() {
|
|
QSet<QString> ids;
|
|
const QList<Game *> games = m_gameModel->allGames();
|
|
for (Game *game : games) {
|
|
if (game) {
|
|
ids.insert(game->id());
|
|
}
|
|
}
|
|
return ids;
|
|
}();
|
|
|
|
setImporting(true);
|
|
setImportStatus(tr("Scanning RetroArch playlists..."));
|
|
|
|
[[maybe_unused]] auto future = QtConcurrent::run([this, existingIds]() {
|
|
RetroArchImporter importer;
|
|
QList<Game *> games = importer.importGames();
|
|
QSet<QString> seenIds = existingIds;
|
|
QList<Game *> newGames;
|
|
newGames.reserve(games.size());
|
|
|
|
for (Game *game : games) {
|
|
if (!game) {
|
|
continue;
|
|
}
|
|
|
|
const QString id = game->id();
|
|
if (id.isEmpty() || seenIds.contains(id)) {
|
|
delete game;
|
|
continue;
|
|
}
|
|
|
|
seenIds.insert(id);
|
|
newGames.append(game);
|
|
}
|
|
|
|
for (Game *game : newGames) {
|
|
game->moveToThread(this->thread());
|
|
game->setParent(nullptr);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, newGames]() {
|
|
for (Game *game : newGames) {
|
|
m_gameModel->addGame(game);
|
|
}
|
|
setImportStatus(tr("RetroArch import complete: %1 new games found").arg(newGames.count()));
|
|
setImporting(false);
|
|
saveLibrary();
|
|
Q_EMIT importCompleted(newGames.count());
|
|
},
|
|
Qt::QueuedConnection);
|
|
});
|
|
}
|
|
|
|
void App::clearLibrary()
|
|
{
|
|
m_gameModel->clear();
|
|
saveLibrary();
|
|
}
|
|
|
|
void App::removeMissingGames()
|
|
{
|
|
QList<Game *> gamesToRemove;
|
|
|
|
const QList<Game *> games = m_gameModel->allGames();
|
|
for (Game *game : games) {
|
|
if (!game)
|
|
continue;
|
|
|
|
QString launchCommand = game->launchCommand();
|
|
|
|
// Skip URL-based launchers (Steam, Lutris, etc.)
|
|
if (launchCommand.startsWith(QLatin1String("steam://")) || launchCommand.startsWith(QLatin1String("lutris:"))
|
|
|| launchCommand.startsWith(QLatin1String("xdg-open")) || launchCommand.startsWith(QLatin1String("flatpak run"))
|
|
|| launchCommand.startsWith(QLatin1String("legendary launch")) || launchCommand.startsWith(QLatin1String("bottles"))) {
|
|
continue;
|
|
}
|
|
|
|
// Extract executable path from command
|
|
QString executable = launchCommand;
|
|
|
|
// Handle quoted paths
|
|
if (executable.startsWith(QLatin1Char('"'))) {
|
|
int endQuote = executable.indexOf(QLatin1Char('"'), 1);
|
|
if (endQuote > 0) {
|
|
executable = executable.mid(1, endQuote - 1);
|
|
}
|
|
} else {
|
|
// Take first word as executable
|
|
int space = executable.indexOf(QLatin1Char(' '));
|
|
if (space > 0) {
|
|
executable = executable.left(space);
|
|
}
|
|
}
|
|
|
|
// Check if executable exists
|
|
if (!executable.isEmpty() && !QFile::exists(executable)) {
|
|
gamesToRemove.append(game);
|
|
}
|
|
}
|
|
|
|
// Remove missing games
|
|
for (Game *game : gamesToRemove) {
|
|
m_gameModel->removeGame(game->id());
|
|
}
|
|
|
|
if (!gamesToRemove.isEmpty()) {
|
|
saveLibrary();
|
|
}
|
|
}
|
|
|
|
void App::saveLibrary()
|
|
{
|
|
QString dataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
|
QDir dir(dataPath);
|
|
if (!dir.exists()) {
|
|
dir.mkpath(dataPath);
|
|
}
|
|
|
|
QFile file(dataPath + QStringLiteral("/library.json"));
|
|
if (!file.open(QIODevice::WriteOnly)) {
|
|
qWarning() << "Failed to save library:" << file.errorString();
|
|
return;
|
|
}
|
|
|
|
QJsonArray gamesArray;
|
|
const QList<Game *> games = m_gameModel->allGames();
|
|
for (Game *game : games) {
|
|
if (game) {
|
|
gamesArray.append(game->toJson());
|
|
}
|
|
}
|
|
|
|
QJsonDocument doc(gamesArray);
|
|
file.write(doc.toJson());
|
|
}
|
|
|
|
void App::loadLibrary()
|
|
{
|
|
QString dataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
|
QFile file(dataPath + QStringLiteral("/library.json"));
|
|
|
|
if (!file.exists()) {
|
|
return;
|
|
}
|
|
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
qWarning() << "Failed to load library:" << file.errorString();
|
|
return;
|
|
}
|
|
|
|
const QByteArray data = file.readAll();
|
|
QJsonParseError parseError;
|
|
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
|
|
if (parseError.error != QJsonParseError::NoError) {
|
|
qWarning() << "Failed to parse library.json:" << parseError.errorString() << "at offset" << parseError.offset;
|
|
return;
|
|
}
|
|
|
|
QJsonArray gamesArray;
|
|
if (doc.isArray()) {
|
|
gamesArray = doc.array();
|
|
} else if (doc.isObject()) {
|
|
const QJsonObject rootObj = doc.object();
|
|
const QJsonValue gamesValue = rootObj.value(QStringLiteral("games"));
|
|
if (gamesValue.isArray()) {
|
|
gamesArray = gamesValue.toArray();
|
|
} else {
|
|
qWarning() << "Invalid library.json format: expected a JSON array (or an object with a 'games' array)";
|
|
return;
|
|
}
|
|
} else {
|
|
qWarning() << "Invalid library.json format: expected a JSON array (or an object with a 'games' array)";
|
|
return;
|
|
}
|
|
|
|
m_gameModel->clear();
|
|
|
|
for (int i = 0; i < gamesArray.size(); ++i) {
|
|
const QJsonValue value = gamesArray.at(i);
|
|
if (!value.isObject()) {
|
|
qWarning() << "Skipping invalid library entry (not an object) at index" << i;
|
|
continue;
|
|
}
|
|
|
|
Game *game = Game::fromJson(value.toObject(), this);
|
|
if (!game) {
|
|
qWarning() << "Skipping invalid game entry at index" << i;
|
|
continue;
|
|
}
|
|
|
|
m_gameModel->addGame(game);
|
|
}
|
|
}
|
|
|
|
Game *App::createGame(const QString &name, const QString &launchCommand)
|
|
{
|
|
if (name.isEmpty() || launchCommand.isEmpty()) {
|
|
return nullptr;
|
|
}
|
|
|
|
Game *game = new Game(this);
|
|
game->setName(name);
|
|
game->setLaunchCommand(launchCommand);
|
|
game->setPlatform(QStringLiteral("Manual"));
|
|
game->setPlatformId(QStringLiteral("manual"));
|
|
|
|
m_gameModel->addGame(game);
|
|
saveLibrary();
|
|
|
|
return game;
|
|
}
|
|
|
|
void App::removeGame(Game *game)
|
|
{
|
|
if (!game) {
|
|
return;
|
|
}
|
|
|
|
// Store in removed games map for potential undo
|
|
m_removedGames[game->id()] = game->toJson();
|
|
|
|
m_gameModel->removeGame(game->id());
|
|
saveLibrary();
|
|
}
|
|
|
|
void App::restoreGame(const QString &gameId)
|
|
{
|
|
if (!m_removedGames.contains(gameId)) {
|
|
return;
|
|
}
|
|
|
|
Game *game = Game::fromJson(m_removedGames[gameId], this);
|
|
if (game) {
|
|
m_gameModel->addGame(game);
|
|
m_removedGames.remove(gameId);
|
|
saveLibrary();
|
|
}
|
|
}
|
|
|
|
bool App::setCoverFromFile(Game *game, const QString &filePath)
|
|
{
|
|
if (!game || filePath.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
QFile sourceFile(filePath);
|
|
if (!sourceFile.exists()) {
|
|
return false;
|
|
}
|
|
|
|
// Create covers directory
|
|
QString coversPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/covers");
|
|
QDir dir(coversPath);
|
|
if (!dir.exists()) {
|
|
dir.mkpath(coversPath);
|
|
}
|
|
|
|
// Determine file extension
|
|
QFileInfo fileInfo(filePath);
|
|
QString extension = fileInfo.suffix().toLower();
|
|
if (extension.isEmpty()) {
|
|
extension = QStringLiteral("jpg");
|
|
}
|
|
|
|
QString destFileName = game->id() + QStringLiteral(".") + extension;
|
|
QString destPath = coversPath + QStringLiteral("/") + destFileName;
|
|
|
|
// Remove existing cover if any
|
|
QFile destFile(destPath);
|
|
if (destFile.exists()) {
|
|
destFile.remove();
|
|
}
|
|
|
|
// Copy file
|
|
if (!sourceFile.copy(destPath)) {
|
|
return false;
|
|
}
|
|
|
|
// Update game cover URL
|
|
game->setCoverUrl(QUrl::fromLocalFile(destPath));
|
|
saveLibrary();
|
|
|
|
return true;
|
|
}
|