// 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 #include #include #include #include #include #include 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 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 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 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(); // Import from Desktop entries QMetaObject::invokeMethod( this, [this]() { setImportStatus(tr("Scanning desktop entries...")); }, Qt::QueuedConnection); DesktopImporter desktopImporter; QList 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 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 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 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 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 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(); // 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 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 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 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 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 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 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 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 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 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 gamesToRemove; const QList 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 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; } 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; }