2026-01-18 12:13:07 +00:00
|
|
|
// 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 <QStandardPaths>
|
|
|
|
|
#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_config(new Config(this))
|
|
|
|
|
{
|
|
|
|
|
loadLibrary();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
setImporting(true);
|
|
|
|
|
setImportStatus(tr("Importing games..."));
|
|
|
|
|
|
|
|
|
|
QtConcurrent::run([this]() {
|
|
|
|
|
int totalCount = 0;
|
|
|
|
|
|
|
|
|
|
// Import from Steam
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this]() {
|
|
|
|
|
setImportStatus(tr("Scanning Steam library..."));
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
|
|
|
|
|
SteamImporter steamImporter;
|
|
|
|
|
QList<Game *> steamGames = steamImporter.importGames();
|
|
|
|
|
for (Game *game : steamGames) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, steamGames]() {
|
|
|
|
|
for (Game *game : steamGames) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
totalCount += steamGames.count();
|
|
|
|
|
|
|
|
|
|
// Import from Lutris
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this]() {
|
|
|
|
|
setImportStatus(tr("Scanning Lutris library..."));
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
|
|
|
|
|
LutrisImporter lutrisImporter;
|
|
|
|
|
QList<Game *> lutrisGames = lutrisImporter.importGames();
|
|
|
|
|
for (Game *game : lutrisGames) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, lutrisGames]() {
|
|
|
|
|
for (Game *game : lutrisGames) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
totalCount += lutrisGames.count();
|
|
|
|
|
|
|
|
|
|
// Import from Heroic
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this]() {
|
|
|
|
|
setImportStatus(tr("Scanning Heroic library..."));
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
|
|
|
|
|
HeroicImporter heroicImporter;
|
|
|
|
|
QList<Game *> heroicGames = heroicImporter.importGames();
|
|
|
|
|
for (Game *game : heroicGames) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, heroicGames]() {
|
|
|
|
|
for (Game *game : heroicGames) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
totalCount += heroicGames.count();
|
|
|
|
|
|
2026-01-19 23:13:11 +00:00
|
|
|
// Import from Desktop entries
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this]() {
|
|
|
|
|
setImportStatus(tr("Scanning desktop entries..."));
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
|
|
|
|
|
DesktopImporter desktopImporter;
|
|
|
|
|
QList<Game *> desktopGames = desktopImporter.importGames();
|
|
|
|
|
for (Game *game : desktopGames) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, desktopGames]() {
|
|
|
|
|
for (Game *game : desktopGames) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
totalCount += desktopGames.count();
|
|
|
|
|
|
|
|
|
|
// Import from Bottles
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this]() {
|
|
|
|
|
setImportStatus(tr("Scanning Bottles..."));
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
|
|
|
|
|
BottlesImporter bottlesImporter;
|
|
|
|
|
QList<Game *> bottlesGames = bottlesImporter.importGames();
|
|
|
|
|
for (Game *game : bottlesGames) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, bottlesGames]() {
|
|
|
|
|
for (Game *game : bottlesGames) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
totalCount += bottlesGames.count();
|
|
|
|
|
|
|
|
|
|
// Import from Flatpak
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this]() {
|
|
|
|
|
setImportStatus(tr("Scanning Flatpak games..."));
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
|
|
|
|
|
FlatpakImporter flatpakImporter;
|
|
|
|
|
QList<Game *> flatpakGames = flatpakImporter.importGames();
|
|
|
|
|
for (Game *game : flatpakGames) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, flatpakGames]() {
|
|
|
|
|
for (Game *game : flatpakGames) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
totalCount += flatpakGames.count();
|
|
|
|
|
|
|
|
|
|
// Import from itch.io
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this]() {
|
|
|
|
|
setImportStatus(tr("Scanning itch.io library..."));
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
|
|
|
|
|
ItchImporter itchImporter;
|
|
|
|
|
QList<Game *> itchGames = itchImporter.importGames();
|
|
|
|
|
for (Game *game : itchGames) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, itchGames]() {
|
|
|
|
|
for (Game *game : itchGames) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
totalCount += itchGames.count();
|
|
|
|
|
|
|
|
|
|
// Import from Legendary
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this]() {
|
|
|
|
|
setImportStatus(tr("Scanning Legendary library..."));
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
|
|
|
|
|
LegendaryImporter legendaryImporter;
|
|
|
|
|
QList<Game *> legendaryGames = legendaryImporter.importGames();
|
|
|
|
|
for (Game *game : legendaryGames) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, legendaryGames]() {
|
|
|
|
|
for (Game *game : legendaryGames) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
totalCount += legendaryGames.count();
|
|
|
|
|
|
|
|
|
|
// Import from RetroArch
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this]() {
|
|
|
|
|
setImportStatus(tr("Scanning RetroArch playlists..."));
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
|
|
|
|
|
RetroArchImporter retroArchImporter;
|
|
|
|
|
QList<Game *> retroArchGames = retroArchImporter.importGames();
|
|
|
|
|
for (Game *game : retroArchGames) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, retroArchGames]() {
|
|
|
|
|
for (Game *game : retroArchGames) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
totalCount += retroArchGames.count();
|
|
|
|
|
|
2026-01-18 12:13:07 +00:00
|
|
|
// Complete
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, totalCount]() {
|
|
|
|
|
setImportStatus(tr("Import complete: %1 games found").arg(totalCount));
|
|
|
|
|
setImporting(false);
|
|
|
|
|
saveLibrary();
|
|
|
|
|
Q_EMIT importCompleted(totalCount);
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void App::importFromSteam()
|
|
|
|
|
{
|
|
|
|
|
if (m_importing)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
setImporting(true);
|
|
|
|
|
setImportStatus(tr("Scanning Steam library..."));
|
|
|
|
|
|
|
|
|
|
QtConcurrent::run([this]() {
|
|
|
|
|
SteamImporter importer;
|
|
|
|
|
QList<Game *> games = importer.importGames();
|
|
|
|
|
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, games]() {
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
setImportStatus(tr("Steam import complete: %1 games found").arg(games.count()));
|
|
|
|
|
setImporting(false);
|
|
|
|
|
saveLibrary();
|
|
|
|
|
Q_EMIT importCompleted(games.count());
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void App::importFromLutris()
|
|
|
|
|
{
|
|
|
|
|
if (m_importing)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
setImporting(true);
|
|
|
|
|
setImportStatus(tr("Scanning Lutris library..."));
|
|
|
|
|
|
|
|
|
|
QtConcurrent::run([this]() {
|
|
|
|
|
LutrisImporter importer;
|
|
|
|
|
QList<Game *> games = importer.importGames();
|
|
|
|
|
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, games]() {
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
setImportStatus(tr("Lutris import complete: %1 games found").arg(games.count()));
|
|
|
|
|
setImporting(false);
|
|
|
|
|
saveLibrary();
|
|
|
|
|
Q_EMIT importCompleted(games.count());
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void App::importFromHeroic()
|
|
|
|
|
{
|
|
|
|
|
if (m_importing)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
setImporting(true);
|
|
|
|
|
setImportStatus(tr("Scanning Heroic library..."));
|
|
|
|
|
|
|
|
|
|
QtConcurrent::run([this]() {
|
|
|
|
|
HeroicImporter importer;
|
|
|
|
|
QList<Game *> games = importer.importGames();
|
|
|
|
|
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, games]() {
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
setImportStatus(tr("Heroic import complete: %1 games found").arg(games.count()));
|
|
|
|
|
setImporting(false);
|
|
|
|
|
saveLibrary();
|
|
|
|
|
Q_EMIT importCompleted(games.count());
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void App::importFromDesktop()
|
|
|
|
|
{
|
|
|
|
|
if (m_importing)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
setImporting(true);
|
|
|
|
|
setImportStatus(tr("Scanning desktop entries..."));
|
|
|
|
|
|
|
|
|
|
QtConcurrent::run([this]() {
|
|
|
|
|
DesktopImporter importer;
|
|
|
|
|
QList<Game *> games = importer.importGames();
|
|
|
|
|
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, games]() {
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
setImportStatus(tr("Desktop import complete: %1 games found").arg(games.count()));
|
|
|
|
|
setImporting(false);
|
|
|
|
|
saveLibrary();
|
|
|
|
|
Q_EMIT importCompleted(games.count());
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void App::importFromBottles()
|
|
|
|
|
{
|
|
|
|
|
if (m_importing)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
setImporting(true);
|
|
|
|
|
setImportStatus(tr("Scanning Bottles..."));
|
|
|
|
|
|
|
|
|
|
QtConcurrent::run([this]() {
|
|
|
|
|
BottlesImporter importer;
|
|
|
|
|
QList<Game *> games = importer.importGames();
|
|
|
|
|
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, games]() {
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
setImportStatus(tr("Bottles import complete: %1 games found").arg(games.count()));
|
|
|
|
|
setImporting(false);
|
|
|
|
|
saveLibrary();
|
|
|
|
|
Q_EMIT importCompleted(games.count());
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void App::importFromFlatpak()
|
|
|
|
|
{
|
|
|
|
|
if (m_importing)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
setImporting(true);
|
|
|
|
|
setImportStatus(tr("Scanning Flatpak games..."));
|
|
|
|
|
|
|
|
|
|
QtConcurrent::run([this]() {
|
|
|
|
|
FlatpakImporter importer;
|
|
|
|
|
QList<Game *> games = importer.importGames();
|
|
|
|
|
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, games]() {
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
setImportStatus(tr("Flatpak import complete: %1 games found").arg(games.count()));
|
|
|
|
|
setImporting(false);
|
|
|
|
|
saveLibrary();
|
|
|
|
|
Q_EMIT importCompleted(games.count());
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void App::importFromItch()
|
|
|
|
|
{
|
|
|
|
|
if (m_importing)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
setImporting(true);
|
|
|
|
|
setImportStatus(tr("Scanning itch.io library..."));
|
|
|
|
|
|
|
|
|
|
QtConcurrent::run([this]() {
|
|
|
|
|
ItchImporter importer;
|
|
|
|
|
QList<Game *> games = importer.importGames();
|
|
|
|
|
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, games]() {
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
setImportStatus(tr("itch.io import complete: %1 games found").arg(games.count()));
|
|
|
|
|
setImporting(false);
|
|
|
|
|
saveLibrary();
|
|
|
|
|
Q_EMIT importCompleted(games.count());
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void App::importFromLegendary()
|
|
|
|
|
{
|
|
|
|
|
if (m_importing)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
setImporting(true);
|
|
|
|
|
setImportStatus(tr("Scanning Legendary library..."));
|
|
|
|
|
|
|
|
|
|
QtConcurrent::run([this]() {
|
|
|
|
|
LegendaryImporter importer;
|
|
|
|
|
QList<Game *> games = importer.importGames();
|
|
|
|
|
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, games]() {
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
setImportStatus(tr("Legendary import complete: %1 games found").arg(games.count()));
|
|
|
|
|
setImporting(false);
|
|
|
|
|
saveLibrary();
|
|
|
|
|
Q_EMIT importCompleted(games.count());
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void App::importFromRetroArch()
|
|
|
|
|
{
|
|
|
|
|
if (m_importing)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
setImporting(true);
|
|
|
|
|
setImportStatus(tr("Scanning RetroArch playlists..."));
|
|
|
|
|
|
|
|
|
|
QtConcurrent::run([this]() {
|
|
|
|
|
RetroArchImporter importer;
|
|
|
|
|
QList<Game *> games = importer.importGames();
|
|
|
|
|
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
game->moveToThread(this->thread());
|
|
|
|
|
game->setParent(nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMetaObject::invokeMethod(
|
|
|
|
|
this,
|
|
|
|
|
[this, games]() {
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
m_gameModel->addGame(game);
|
|
|
|
|
}
|
|
|
|
|
setImportStatus(tr("RetroArch import complete: %1 games found").arg(games.count()));
|
|
|
|
|
setImporting(false);
|
|
|
|
|
saveLibrary();
|
|
|
|
|
Q_EMIT importCompleted(games.count());
|
|
|
|
|
},
|
|
|
|
|
Qt::QueuedConnection);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void App::clearLibrary()
|
|
|
|
|
{
|
|
|
|
|
m_gameModel->clear();
|
|
|
|
|
saveLibrary();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void App::removeMissingGames()
|
|
|
|
|
{
|
|
|
|
|
QList<Game *> gamesToRemove;
|
|
|
|
|
|
2026-01-19 23:13:11 +00:00
|
|
|
const QList<Game *> games = m_gameModel->allGames();
|
|
|
|
|
for (Game *game : games) {
|
2026-01-18 12:13:07 +00:00
|
|
|
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;
|
2026-01-19 23:13:11 +00:00
|
|
|
const QList<Game *> games = m_gameModel->allGames();
|
|
|
|
|
for (Game *game : games) {
|
2026-01-18 12:13:07 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
|
|
|
|
if (!doc.isArray()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonArray gamesArray = doc.array();
|
|
|
|
|
for (const QJsonValue &value : gamesArray) {
|
|
|
|
|
if (value.isObject()) {
|
|
|
|
|
Game *game = Game::fromJson(value.toObject(), this);
|
|
|
|
|
if (game) {
|
|
|
|
|
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;
|
|
|
|
|
}
|