mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-02-09 21:13:08 +00:00
Merge branch 'fix/incremental-import' into 'master'
Add launch profiles MVP See merge request marcoa/a-la-karte!4
This commit is contained in:
commit
0668efbfa0
10 changed files with 873 additions and 131 deletions
450
src/app.cpp
450
src/app.cpp
|
|
@ -18,7 +18,9 @@
|
|||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QSet>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
#include <QtConcurrent>
|
||||
|
||||
App *App::s_instance = nullptr;
|
||||
|
|
@ -142,6 +144,12 @@ App::App(QObject *parent)
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (m_config->autoImportOnStartup()) {
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
importAllGames();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
App *App::instance()
|
||||
|
|
@ -215,18 +223,17 @@ void App::importAllGames()
|
|||
if (m_importing)
|
||||
return;
|
||||
|
||||
const bool anyEnabled = m_config->importSteam() || m_config->importLutris() || m_config->importHeroic() || m_config->importDesktop()
|
||||
|| m_config->importBottles() || m_config->importFlatpak() || m_config->importItch() || m_config->importLegendary() || m_config->importRetroArch();
|
||||
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 doSteam = m_config->importSteam() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Steam"));
|
||||
const bool doLutris = m_config->importLutris() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Lutris"));
|
||||
const bool doHeroic = m_config->importHeroic() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Heroic"));
|
||||
const bool doDesktop = m_config->importDesktop() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Desktop"));
|
||||
const bool doBottles = m_config->importBottles() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Bottles"));
|
||||
const bool doFlatpak = m_config->importFlatpak() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Flatpak"));
|
||||
const bool doItch = m_config->importItch() && !m_gameModel->hasPlatformPrefix(QStringLiteral("itch.io"));
|
||||
const bool doLegendary = m_config->importLegendary() && !m_gameModel->hasPlatformPrefix(QStringLiteral("Legendary"));
|
||||
const bool doRetroArch = m_config->importRetroArch() && !m_gameModel->hasPlatformPrefix(QStringLiteral("RetroArch"));
|
||||
const bool anyEnabled = doSteam || doLutris || doHeroic || doDesktop || doBottles || doFlatpak || doItch || doLegendary || doRetroArch;
|
||||
|
||||
if (!anyEnabled) {
|
||||
setImportStatus(tr("No import sources enabled"));
|
||||
|
|
@ -234,17 +241,55 @@ void App::importAllGames()
|
|||
return;
|
||||
}
|
||||
|
||||
if (!(doSteam || doLutris || doHeroic || doDesktop || doBottles || doFlatpak || doItch || doLegendary || doRetroArch)) {
|
||||
setImportStatus(tr("All enabled sources already imported"));
|
||||
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]() {
|
||||
[[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) {
|
||||
|
|
@ -256,7 +301,7 @@ void App::importAllGames()
|
|||
Qt::QueuedConnection);
|
||||
|
||||
SteamImporter steamImporter;
|
||||
QList<Game *> steamGames = steamImporter.importGames();
|
||||
QList<Game *> steamGames = keepNewGames(steamImporter.importGames());
|
||||
for (Game *game : steamGames) {
|
||||
game->moveToThread(this->thread());
|
||||
game->setParent(nullptr);
|
||||
|
|
@ -291,7 +336,7 @@ void App::importAllGames()
|
|||
Qt::QueuedConnection);
|
||||
|
||||
LutrisImporter lutrisImporter;
|
||||
QList<Game *> lutrisGames = lutrisImporter.importGames();
|
||||
QList<Game *> lutrisGames = keepNewGames(lutrisImporter.importGames());
|
||||
for (Game *game : lutrisGames) {
|
||||
game->moveToThread(this->thread());
|
||||
game->setParent(nullptr);
|
||||
|
|
@ -326,7 +371,7 @@ void App::importAllGames()
|
|||
Qt::QueuedConnection);
|
||||
|
||||
HeroicImporter heroicImporter;
|
||||
QList<Game *> heroicGames = heroicImporter.importGames();
|
||||
QList<Game *> heroicGames = keepNewGames(heroicImporter.importGames());
|
||||
for (Game *game : heroicGames) {
|
||||
game->moveToThread(this->thread());
|
||||
game->setParent(nullptr);
|
||||
|
|
@ -361,7 +406,7 @@ void App::importAllGames()
|
|||
Qt::QueuedConnection);
|
||||
|
||||
DesktopImporter desktopImporter;
|
||||
QList<Game *> desktopGames = desktopImporter.importGames();
|
||||
QList<Game *> desktopGames = keepNewGames(desktopImporter.importGames());
|
||||
for (Game *game : desktopGames) {
|
||||
game->moveToThread(this->thread());
|
||||
game->setParent(nullptr);
|
||||
|
|
@ -396,7 +441,7 @@ void App::importAllGames()
|
|||
Qt::QueuedConnection);
|
||||
|
||||
BottlesImporter bottlesImporter;
|
||||
QList<Game *> bottlesGames = bottlesImporter.importGames();
|
||||
QList<Game *> bottlesGames = keepNewGames(bottlesImporter.importGames());
|
||||
for (Game *game : bottlesGames) {
|
||||
game->moveToThread(this->thread());
|
||||
game->setParent(nullptr);
|
||||
|
|
@ -431,7 +476,7 @@ void App::importAllGames()
|
|||
Qt::QueuedConnection);
|
||||
|
||||
FlatpakImporter flatpakImporter;
|
||||
QList<Game *> flatpakGames = flatpakImporter.importGames();
|
||||
QList<Game *> flatpakGames = keepNewGames(flatpakImporter.importGames());
|
||||
for (Game *game : flatpakGames) {
|
||||
game->moveToThread(this->thread());
|
||||
game->setParent(nullptr);
|
||||
|
|
@ -466,7 +511,7 @@ void App::importAllGames()
|
|||
Qt::QueuedConnection);
|
||||
|
||||
ItchImporter itchImporter;
|
||||
QList<Game *> itchGames = itchImporter.importGames();
|
||||
QList<Game *> itchGames = keepNewGames(itchImporter.importGames());
|
||||
for (Game *game : itchGames) {
|
||||
game->moveToThread(this->thread());
|
||||
game->setParent(nullptr);
|
||||
|
|
@ -501,7 +546,7 @@ void App::importAllGames()
|
|||
Qt::QueuedConnection);
|
||||
|
||||
LegendaryImporter legendaryImporter;
|
||||
QList<Game *> legendaryGames = legendaryImporter.importGames();
|
||||
QList<Game *> legendaryGames = keepNewGames(legendaryImporter.importGames());
|
||||
for (Game *game : legendaryGames) {
|
||||
game->moveToThread(this->thread());
|
||||
game->setParent(nullptr);
|
||||
|
|
@ -536,7 +581,7 @@ void App::importAllGames()
|
|||
Qt::QueuedConnection);
|
||||
|
||||
RetroArchImporter retroArchImporter;
|
||||
QList<Game *> retroArchGames = retroArchImporter.importGames();
|
||||
QList<Game *> retroArchGames = keepNewGames(retroArchImporter.importGames());
|
||||
for (Game *game : retroArchGames) {
|
||||
game->moveToThread(this->thread());
|
||||
game->setParent(nullptr);
|
||||
|
|
@ -565,7 +610,7 @@ void App::importAllGames()
|
|||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
[this, totalCount]() {
|
||||
setImportStatus(tr("Import complete: %1 games found").arg(totalCount));
|
||||
setImportStatus(tr("Import complete: %1 new games found").arg(totalCount));
|
||||
setImporting(false);
|
||||
saveLibrary();
|
||||
Q_EMIT importCompleted(totalCount);
|
||||
|
|
@ -579,28 +624,57 @@ 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]() {
|
||||
[[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, games]() {
|
||||
for (Game *game : games) {
|
||||
[this, newGames]() {
|
||||
for (Game *game : newGames) {
|
||||
m_gameModel->addGame(game);
|
||||
}
|
||||
setImportStatus(tr("Steam import complete: %1 games found").arg(games.count()));
|
||||
setImportStatus(tr("Steam import complete: %1 new games found").arg(newGames.count()));
|
||||
setImporting(false);
|
||||
saveLibrary();
|
||||
Q_EMIT importCompleted(games.count());
|
||||
Q_EMIT importCompleted(newGames.count());
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
});
|
||||
|
|
@ -611,28 +685,57 @@ 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]() {
|
||||
[[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, games]() {
|
||||
for (Game *game : games) {
|
||||
[this, newGames]() {
|
||||
for (Game *game : newGames) {
|
||||
m_gameModel->addGame(game);
|
||||
}
|
||||
setImportStatus(tr("Lutris import complete: %1 games found").arg(games.count()));
|
||||
setImportStatus(tr("Lutris import complete: %1 new games found").arg(newGames.count()));
|
||||
setImporting(false);
|
||||
saveLibrary();
|
||||
Q_EMIT importCompleted(games.count());
|
||||
Q_EMIT importCompleted(newGames.count());
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
});
|
||||
|
|
@ -643,28 +746,57 @@ 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]() {
|
||||
[[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, games]() {
|
||||
for (Game *game : games) {
|
||||
[this, newGames]() {
|
||||
for (Game *game : newGames) {
|
||||
m_gameModel->addGame(game);
|
||||
}
|
||||
setImportStatus(tr("Heroic import complete: %1 games found").arg(games.count()));
|
||||
setImportStatus(tr("Heroic import complete: %1 new games found").arg(newGames.count()));
|
||||
setImporting(false);
|
||||
saveLibrary();
|
||||
Q_EMIT importCompleted(games.count());
|
||||
Q_EMIT importCompleted(newGames.count());
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
});
|
||||
|
|
@ -675,28 +807,57 @@ 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]() {
|
||||
[[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, games]() {
|
||||
for (Game *game : games) {
|
||||
[this, newGames]() {
|
||||
for (Game *game : newGames) {
|
||||
m_gameModel->addGame(game);
|
||||
}
|
||||
setImportStatus(tr("Desktop import complete: %1 games found").arg(games.count()));
|
||||
setImportStatus(tr("Desktop import complete: %1 new games found").arg(newGames.count()));
|
||||
setImporting(false);
|
||||
saveLibrary();
|
||||
Q_EMIT importCompleted(games.count());
|
||||
Q_EMIT importCompleted(newGames.count());
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
});
|
||||
|
|
@ -707,28 +868,57 @@ 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]() {
|
||||
[[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, games]() {
|
||||
for (Game *game : games) {
|
||||
[this, newGames]() {
|
||||
for (Game *game : newGames) {
|
||||
m_gameModel->addGame(game);
|
||||
}
|
||||
setImportStatus(tr("Bottles import complete: %1 games found").arg(games.count()));
|
||||
setImportStatus(tr("Bottles import complete: %1 new games found").arg(newGames.count()));
|
||||
setImporting(false);
|
||||
saveLibrary();
|
||||
Q_EMIT importCompleted(games.count());
|
||||
Q_EMIT importCompleted(newGames.count());
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
});
|
||||
|
|
@ -739,28 +929,57 @@ 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]() {
|
||||
[[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, games]() {
|
||||
for (Game *game : games) {
|
||||
[this, newGames]() {
|
||||
for (Game *game : newGames) {
|
||||
m_gameModel->addGame(game);
|
||||
}
|
||||
setImportStatus(tr("Flatpak import complete: %1 games found").arg(games.count()));
|
||||
setImportStatus(tr("Flatpak import complete: %1 new games found").arg(newGames.count()));
|
||||
setImporting(false);
|
||||
saveLibrary();
|
||||
Q_EMIT importCompleted(games.count());
|
||||
Q_EMIT importCompleted(newGames.count());
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
});
|
||||
|
|
@ -771,28 +990,57 @@ 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]() {
|
||||
[[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, games]() {
|
||||
for (Game *game : games) {
|
||||
[this, newGames]() {
|
||||
for (Game *game : newGames) {
|
||||
m_gameModel->addGame(game);
|
||||
}
|
||||
setImportStatus(tr("itch.io import complete: %1 games found").arg(games.count()));
|
||||
setImportStatus(tr("itch.io import complete: %1 new games found").arg(newGames.count()));
|
||||
setImporting(false);
|
||||
saveLibrary();
|
||||
Q_EMIT importCompleted(games.count());
|
||||
Q_EMIT importCompleted(newGames.count());
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
});
|
||||
|
|
@ -803,28 +1051,57 @@ 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]() {
|
||||
[[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, games]() {
|
||||
for (Game *game : games) {
|
||||
[this, newGames]() {
|
||||
for (Game *game : newGames) {
|
||||
m_gameModel->addGame(game);
|
||||
}
|
||||
setImportStatus(tr("Legendary import complete: %1 games found").arg(games.count()));
|
||||
setImportStatus(tr("Legendary import complete: %1 new games found").arg(newGames.count()));
|
||||
setImporting(false);
|
||||
saveLibrary();
|
||||
Q_EMIT importCompleted(games.count());
|
||||
Q_EMIT importCompleted(newGames.count());
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
});
|
||||
|
|
@ -835,28 +1112,57 @@ 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]() {
|
||||
[[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, games]() {
|
||||
for (Game *game : games) {
|
||||
[this, newGames]() {
|
||||
for (Game *game : newGames) {
|
||||
m_gameModel->addGame(game);
|
||||
}
|
||||
setImportStatus(tr("RetroArch import complete: %1 games found").arg(games.count()));
|
||||
setImportStatus(tr("RetroArch import complete: %1 new games found").arg(newGames.count()));
|
||||
setImporting(false);
|
||||
saveLibrary();
|
||||
Q_EMIT importCompleted(games.count());
|
||||
Q_EMIT importCompleted(newGames.count());
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,7 +40,13 @@ QStringList DesktopImporter::getDesktopFilePaths() const
|
|||
// Add common system locations
|
||||
appDirs << QStringLiteral("/usr/share/applications");
|
||||
appDirs << QStringLiteral("/usr/local/share/applications");
|
||||
appDirs << expandPath(QStringLiteral("~/.local/share/applications"));
|
||||
|
||||
const QString xdgDataHome = qEnvironmentVariable("XDG_DATA_HOME");
|
||||
if (!xdgDataHome.isEmpty()) {
|
||||
appDirs << QDir(xdgDataHome).absoluteFilePath(QStringLiteral("applications"));
|
||||
} else {
|
||||
appDirs << expandPath(QStringLiteral("~/.local/share/applications"));
|
||||
}
|
||||
|
||||
// Flatpak export directories
|
||||
appDirs << expandPath(QStringLiteral("~/.local/share/flatpak/exports/share/applications"));
|
||||
|
|
|
|||
232
src/game.cpp
232
src/game.cpp
|
|
@ -185,6 +185,134 @@ void Game::setLaunchPrefixPath(const QString &path)
|
|||
}
|
||||
}
|
||||
|
||||
QString Game::activeLaunchProfile() const
|
||||
{
|
||||
return m_activeLaunchProfile;
|
||||
}
|
||||
|
||||
void Game::setActiveLaunchProfile(const QString &profileId)
|
||||
{
|
||||
const QString normalized = profileId.trimmed().isEmpty() ? QStringLiteral("default") : profileId.trimmed();
|
||||
if (m_activeLaunchProfile != normalized) {
|
||||
m_activeLaunchProfile = normalized;
|
||||
Q_EMIT activeLaunchProfileChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QVariantMap Game::launchProfileConfig(const QString &profileId) const
|
||||
{
|
||||
const QString id = profileId.trimmed();
|
||||
if (id.isEmpty() || id == QLatin1String("default")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QVariant value = m_launchProfiles.value(id);
|
||||
if (!value.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return value.toMap();
|
||||
}
|
||||
|
||||
void Game::setLaunchProfileConfig(const QString &profileId, const QVariantMap &config)
|
||||
{
|
||||
const QString id = profileId.trimmed();
|
||||
if (id.isEmpty() || id == QLatin1String("default")) {
|
||||
return;
|
||||
}
|
||||
|
||||
QVariantMap cleaned;
|
||||
|
||||
const QString runner = config.value(QStringLiteral("runner")).toString().trimmed();
|
||||
if (!runner.isEmpty()) {
|
||||
cleaned.insert(QStringLiteral("runner"), runner);
|
||||
}
|
||||
|
||||
const QString runnerPath = config.value(QStringLiteral("runnerPath")).toString().trimmed();
|
||||
if (!runnerPath.isEmpty()) {
|
||||
cleaned.insert(QStringLiteral("runnerPath"), runnerPath);
|
||||
}
|
||||
|
||||
const QString prefixPath = config.value(QStringLiteral("prefixPath")).toString().trimmed();
|
||||
if (!prefixPath.isEmpty()) {
|
||||
cleaned.insert(QStringLiteral("prefixPath"), prefixPath);
|
||||
}
|
||||
|
||||
const QVariantMap env = config.value(QStringLiteral("env")).toMap();
|
||||
if (!env.isEmpty()) {
|
||||
cleaned.insert(QStringLiteral("env"), env);
|
||||
}
|
||||
|
||||
if (cleaned.isEmpty()) {
|
||||
m_launchProfiles.remove(id);
|
||||
} else {
|
||||
m_launchProfiles.insert(id, cleaned);
|
||||
}
|
||||
}
|
||||
|
||||
QVariantMap Game::effectiveLaunchConfig() const
|
||||
{
|
||||
return effectiveLaunchConfigForProfile(m_activeLaunchProfile);
|
||||
}
|
||||
|
||||
QVariantMap Game::effectiveLaunchConfigForProfile(const QString &profileId) const
|
||||
{
|
||||
QVariantMap result;
|
||||
|
||||
if (!m_launchRunner.isEmpty()) {
|
||||
result.insert(QStringLiteral("runner"), m_launchRunner);
|
||||
}
|
||||
if (!m_launchRunnerPath.isEmpty()) {
|
||||
result.insert(QStringLiteral("runnerPath"), m_launchRunnerPath);
|
||||
}
|
||||
if (!m_launchPrefixPath.isEmpty()) {
|
||||
result.insert(QStringLiteral("prefixPath"), m_launchPrefixPath);
|
||||
}
|
||||
if (!m_launchEnv.isEmpty()) {
|
||||
result.insert(QStringLiteral("env"), m_launchEnv);
|
||||
}
|
||||
|
||||
const QString id = profileId.trimmed();
|
||||
if (id.isEmpty() || id == QLatin1String("default")) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const QVariantMap profileConfig = launchProfileConfig(id);
|
||||
if (profileConfig.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const QString runner = profileConfig.value(QStringLiteral("runner")).toString().trimmed();
|
||||
if (!runner.isEmpty()) {
|
||||
result.insert(QStringLiteral("runner"), runner);
|
||||
}
|
||||
|
||||
const QString runnerPath = profileConfig.value(QStringLiteral("runnerPath")).toString().trimmed();
|
||||
if (!runnerPath.isEmpty()) {
|
||||
result.insert(QStringLiteral("runnerPath"), runnerPath);
|
||||
}
|
||||
|
||||
const QString prefixPath = profileConfig.value(QStringLiteral("prefixPath")).toString().trimmed();
|
||||
if (!prefixPath.isEmpty()) {
|
||||
result.insert(QStringLiteral("prefixPath"), prefixPath);
|
||||
}
|
||||
|
||||
const QVariantMap profileEnv = profileConfig.value(QStringLiteral("env")).toMap();
|
||||
if (!profileEnv.isEmpty()) {
|
||||
QVariantMap mergedEnv;
|
||||
const QVariantMap baseEnv = result.value(QStringLiteral("env")).toMap();
|
||||
for (auto it = baseEnv.constBegin(); it != baseEnv.constEnd(); ++it) {
|
||||
mergedEnv.insert(it.key(), it.value());
|
||||
}
|
||||
for (auto it = profileEnv.constBegin(); it != profileEnv.constEnd(); ++it) {
|
||||
mergedEnv.insert(it.key(), it.value());
|
||||
}
|
||||
result.insert(QStringLiteral("env"), mergedEnv);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QString Game::platform() const
|
||||
{
|
||||
return m_platform;
|
||||
|
|
@ -323,7 +451,8 @@ QJsonObject Game::toJson() const
|
|||
obj[QStringLiteral("hidden")] = m_hidden;
|
||||
obj[QStringLiteral("installed")] = m_installed;
|
||||
|
||||
const bool hasLaunchConfig = !m_launchEnv.isEmpty() || !m_launchRunner.isEmpty() || !m_launchRunnerPath.isEmpty() || !m_launchPrefixPath.isEmpty();
|
||||
const bool hasLaunchConfig = !m_launchEnv.isEmpty() || !m_launchRunner.isEmpty() || !m_launchRunnerPath.isEmpty() || !m_launchPrefixPath.isEmpty()
|
||||
|| !m_launchProfiles.isEmpty() || (!m_activeLaunchProfile.isEmpty() && m_activeLaunchProfile != QLatin1String("default"));
|
||||
if (hasLaunchConfig) {
|
||||
QJsonObject launchObj;
|
||||
|
||||
|
|
@ -345,6 +474,57 @@ QJsonObject Game::toJson() const
|
|||
launchObj.insert(QStringLiteral("prefixPath"), m_launchPrefixPath);
|
||||
}
|
||||
|
||||
if (!m_activeLaunchProfile.isEmpty() && m_activeLaunchProfile != QLatin1String("default")) {
|
||||
launchObj.insert(QStringLiteral("activeProfile"), m_activeLaunchProfile);
|
||||
}
|
||||
|
||||
if (!m_launchProfiles.isEmpty()) {
|
||||
QJsonObject profilesObj;
|
||||
for (auto it = m_launchProfiles.constBegin(); it != m_launchProfiles.constEnd(); ++it) {
|
||||
const QString profileId = it.key();
|
||||
if (profileId.isEmpty() || profileId == QLatin1String("default")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QVariantMap profileConfig = it.value().toMap();
|
||||
if (profileConfig.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject profileObj;
|
||||
|
||||
const QString runner = profileConfig.value(QStringLiteral("runner")).toString();
|
||||
if (!runner.isEmpty()) {
|
||||
profileObj.insert(QStringLiteral("runner"), runner);
|
||||
}
|
||||
const QString runnerPath = profileConfig.value(QStringLiteral("runnerPath")).toString();
|
||||
if (!runnerPath.isEmpty()) {
|
||||
profileObj.insert(QStringLiteral("runnerPath"), runnerPath);
|
||||
}
|
||||
const QString prefixPath = profileConfig.value(QStringLiteral("prefixPath")).toString();
|
||||
if (!prefixPath.isEmpty()) {
|
||||
profileObj.insert(QStringLiteral("prefixPath"), prefixPath);
|
||||
}
|
||||
|
||||
const QVariantMap env = profileConfig.value(QStringLiteral("env")).toMap();
|
||||
if (!env.isEmpty()) {
|
||||
QJsonObject envObj;
|
||||
for (auto envIt = env.constBegin(); envIt != env.constEnd(); ++envIt) {
|
||||
envObj.insert(envIt.key(), QJsonValue::fromVariant(envIt.value()));
|
||||
}
|
||||
profileObj.insert(QStringLiteral("env"), envObj);
|
||||
}
|
||||
|
||||
if (!profileObj.isEmpty()) {
|
||||
profilesObj.insert(profileId, profileObj);
|
||||
}
|
||||
}
|
||||
|
||||
if (!profilesObj.isEmpty()) {
|
||||
launchObj.insert(QStringLiteral("profiles"), profilesObj);
|
||||
}
|
||||
}
|
||||
|
||||
obj.insert(QStringLiteral("launch"), launchObj);
|
||||
}
|
||||
|
||||
|
|
@ -393,6 +573,7 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
|
|||
QString runner;
|
||||
QString runnerPath;
|
||||
QString prefixPath;
|
||||
QString activeProfile;
|
||||
const QJsonValue launchValue = json.value(QStringLiteral("launch"));
|
||||
if (launchValue.isObject()) {
|
||||
const QJsonObject launchObj = launchValue.toObject();
|
||||
|
|
@ -404,6 +585,51 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
|
|||
runner = launchObj.value(QStringLiteral("runner")).toString();
|
||||
runnerPath = launchObj.value(QStringLiteral("runnerPath")).toString();
|
||||
prefixPath = launchObj.value(QStringLiteral("prefixPath")).toString();
|
||||
|
||||
activeProfile = launchObj.value(QStringLiteral("activeProfile")).toString();
|
||||
|
||||
const QJsonValue profilesValue = launchObj.value(QStringLiteral("profiles"));
|
||||
if (profilesValue.isObject()) {
|
||||
const QJsonObject profilesObj = profilesValue.toObject();
|
||||
for (auto it = profilesObj.constBegin(); it != profilesObj.constEnd(); ++it) {
|
||||
if (!it.value().isObject()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString profileId = it.key();
|
||||
if (profileId.isEmpty() || profileId == QLatin1String("default")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QJsonObject profileObj = it.value().toObject();
|
||||
QVariantMap profileConfig;
|
||||
|
||||
const QString pRunner = profileObj.value(QStringLiteral("runner")).toString();
|
||||
if (!pRunner.isEmpty()) {
|
||||
profileConfig.insert(QStringLiteral("runner"), pRunner);
|
||||
}
|
||||
const QString pRunnerPath = profileObj.value(QStringLiteral("runnerPath")).toString();
|
||||
if (!pRunnerPath.isEmpty()) {
|
||||
profileConfig.insert(QStringLiteral("runnerPath"), pRunnerPath);
|
||||
}
|
||||
const QString pPrefixPath = profileObj.value(QStringLiteral("prefixPath")).toString();
|
||||
if (!pPrefixPath.isEmpty()) {
|
||||
profileConfig.insert(QStringLiteral("prefixPath"), pPrefixPath);
|
||||
}
|
||||
|
||||
const QJsonValue pEnvValue = profileObj.value(QStringLiteral("env"));
|
||||
if (pEnvValue.isObject()) {
|
||||
const QVariantMap pEnv = pEnvValue.toObject().toVariantMap();
|
||||
if (!pEnv.isEmpty()) {
|
||||
profileConfig.insert(QStringLiteral("env"), pEnv);
|
||||
}
|
||||
}
|
||||
|
||||
if (!profileConfig.isEmpty()) {
|
||||
game->setLaunchProfileConfig(profileId, profileConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const QJsonValue legacyEnvValue = json.value(QStringLiteral("launchEnv"));
|
||||
|
|
@ -435,6 +661,10 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
|
|||
game->setLaunchPrefixPath(prefixPath);
|
||||
}
|
||||
|
||||
if (!activeProfile.isEmpty()) {
|
||||
game->setActiveLaunchProfile(activeProfile);
|
||||
}
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
|
|
|
|||
12
src/game.h
12
src/game.h
|
|
@ -30,6 +30,7 @@ class Game : public QObject
|
|||
Q_PROPERTY(QString launchRunner READ launchRunner WRITE setLaunchRunner NOTIFY launchRunnerChanged)
|
||||
Q_PROPERTY(QString launchRunnerPath READ launchRunnerPath WRITE setLaunchRunnerPath NOTIFY launchRunnerPathChanged)
|
||||
Q_PROPERTY(QString launchPrefixPath READ launchPrefixPath WRITE setLaunchPrefixPath NOTIFY launchPrefixPathChanged)
|
||||
Q_PROPERTY(QString activeLaunchProfile READ activeLaunchProfile WRITE setActiveLaunchProfile NOTIFY activeLaunchProfileChanged)
|
||||
Q_PROPERTY(QString platform READ platform WRITE setPlatform NOTIFY platformChanged)
|
||||
Q_PROPERTY(QString platformId READ platformId WRITE setPlatformId NOTIFY platformIdChanged)
|
||||
Q_PROPERTY(QDateTime dateAdded READ dateAdded WRITE setDateAdded NOTIFY dateAddedChanged)
|
||||
|
|
@ -82,6 +83,14 @@ public:
|
|||
QString launchPrefixPath() const;
|
||||
void setLaunchPrefixPath(const QString &path);
|
||||
|
||||
QString activeLaunchProfile() const;
|
||||
void setActiveLaunchProfile(const QString &profileId);
|
||||
|
||||
Q_INVOKABLE QVariantMap launchProfileConfig(const QString &profileId) const;
|
||||
Q_INVOKABLE void setLaunchProfileConfig(const QString &profileId, const QVariantMap &config);
|
||||
QVariantMap effectiveLaunchConfigForProfile(const QString &profileId) const;
|
||||
QVariantMap effectiveLaunchConfig() const;
|
||||
|
||||
QString platform() const;
|
||||
void setPlatform(const QString &platform);
|
||||
|
||||
|
|
@ -129,6 +138,7 @@ Q_SIGNALS:
|
|||
void launchRunnerChanged();
|
||||
void launchRunnerPathChanged();
|
||||
void launchPrefixPathChanged();
|
||||
void activeLaunchProfileChanged();
|
||||
void platformChanged();
|
||||
void platformIdChanged();
|
||||
void dateAddedChanged();
|
||||
|
|
@ -153,6 +163,8 @@ private:
|
|||
QString m_launchRunner;
|
||||
QString m_launchRunnerPath;
|
||||
QString m_launchPrefixPath;
|
||||
QString m_activeLaunchProfile = QStringLiteral("default");
|
||||
QVariantMap m_launchProfiles;
|
||||
QString m_platform;
|
||||
QString m_platformId;
|
||||
QDateTime m_dateAdded;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
#include "gamelauncher.h"
|
||||
#include "app.h"
|
||||
#include "gamepadmanager.h"
|
||||
#include "inputmanager.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
|
|
@ -108,6 +110,33 @@ static QString discoverDefaultProtonExecutable()
|
|||
return cached;
|
||||
}
|
||||
|
||||
static QString profileIdForCurrentUiMode()
|
||||
{
|
||||
const Config *config = App::instance() ? App::instance()->config() : nullptr;
|
||||
if (!config) {
|
||||
return QStringLiteral("default");
|
||||
}
|
||||
|
||||
if (config->uiMode() == Config::Couch) {
|
||||
return QStringLiteral("couch");
|
||||
}
|
||||
|
||||
if (config->uiMode() == Config::Auto) {
|
||||
GamepadManager *pad = GamepadManager::instance();
|
||||
InputManager *input = InputManager::instance();
|
||||
|
||||
if (pad && pad->connected()) {
|
||||
const bool activeGamepad = input && input->activeInput() == InputManager::Gamepad;
|
||||
const bool noKeyboardMouse = input && !input->hasSeenKeyboardMouse();
|
||||
if (activeGamepad || noKeyboardMouse) {
|
||||
return QStringLiteral("couch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QStringLiteral("default");
|
||||
}
|
||||
|
||||
GameLauncher::GameLauncher(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
|
|
@ -153,10 +182,12 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
|
|||
return info;
|
||||
}
|
||||
|
||||
const QString runner = game->launchRunner().trimmed();
|
||||
const QString runnerPath = game->launchRunnerPath().trimmed();
|
||||
const QString prefixPath = game->launchPrefixPath().trimmed();
|
||||
const QVariantMap launchEnv = game->launchEnv();
|
||||
const QString profileId = profileIdForCurrentUiMode();
|
||||
const QVariantMap effectiveLaunchConfig = game->effectiveLaunchConfigForProfile(profileId);
|
||||
const QString runner = effectiveLaunchConfig.value(QStringLiteral("runner")).toString().trimmed();
|
||||
const QString runnerPath = effectiveLaunchConfig.value(QStringLiteral("runnerPath")).toString().trimmed();
|
||||
const QString prefixPath = effectiveLaunchConfig.value(QStringLiteral("prefixPath")).toString().trimmed();
|
||||
const QVariantMap launchEnv = effectiveLaunchConfig.value(QStringLiteral("env")).toMap();
|
||||
const bool hasLaunchOverrides = !runner.isEmpty() || !runnerPath.isEmpty() || !prefixPath.isEmpty() || !launchEnv.isEmpty();
|
||||
|
||||
info.insert(QStringLiteral("runner"), runner);
|
||||
|
|
@ -297,10 +328,12 @@ void GameLauncher::launchGame(Game *game)
|
|||
return;
|
||||
}
|
||||
|
||||
const QString runner = game->launchRunner().trimmed();
|
||||
const QString runnerPath = game->launchRunnerPath().trimmed();
|
||||
const QString prefixPath = game->launchPrefixPath().trimmed();
|
||||
const QVariantMap launchEnv = game->launchEnv();
|
||||
const QString profileId = profileIdForCurrentUiMode();
|
||||
const QVariantMap effectiveLaunchConfig = game->effectiveLaunchConfigForProfile(profileId);
|
||||
const QString runner = effectiveLaunchConfig.value(QStringLiteral("runner")).toString().trimmed();
|
||||
const QString runnerPath = effectiveLaunchConfig.value(QStringLiteral("runnerPath")).toString().trimmed();
|
||||
const QString prefixPath = effectiveLaunchConfig.value(QStringLiteral("prefixPath")).toString().trimmed();
|
||||
const QVariantMap launchEnv = effectiveLaunchConfig.value(QStringLiteral("env")).toMap();
|
||||
const bool hasLaunchOverrides = !runner.isEmpty() || !runnerPath.isEmpty() || !prefixPath.isEmpty() || !launchEnv.isEmpty();
|
||||
|
||||
// Check if already running
|
||||
|
|
|
|||
46
src/main.cpp
46
src/main.cpp
|
|
@ -8,6 +8,8 @@
|
|||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QQuickStyle>
|
||||
#include <QTextStream>
|
||||
#include <QTimer>
|
||||
|
||||
#include <KAboutData>
|
||||
#include <KCrash>
|
||||
|
|
@ -16,6 +18,7 @@
|
|||
#include <KLocalizedString>
|
||||
|
||||
#include "alakarte-version.h"
|
||||
#include "app.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
|
|
@ -47,15 +50,54 @@ int main(int argc, char *argv[])
|
|||
|
||||
QCommandLineParser parser;
|
||||
aboutData.setupCommandLine(&parser);
|
||||
|
||||
QCommandLineOption importAllAndExitOption(QStringLiteral("import-all-and-exit"), i18n("Import all enabled sources and exit"));
|
||||
QCommandLineOption importDesktopAndExitOption(QStringLiteral("import-desktop-and-exit"), i18n("Import desktop entries and exit"));
|
||||
QCommandLineOption startupAndExitOption(QStringLiteral("startup-and-exit"), i18n("Start without UI and exit after startup auto-import (if enabled)"));
|
||||
parser.addOption(importAllAndExitOption);
|
||||
parser.addOption(importDesktopAndExitOption);
|
||||
parser.addOption(startupAndExitOption);
|
||||
|
||||
parser.process(app);
|
||||
aboutData.processCommandLine(&parser);
|
||||
|
||||
KDBusService service(KDBusService::Unique);
|
||||
|
||||
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
|
||||
QQuickStyle::setStyle(QStringLiteral("org.kde.desktop"));
|
||||
}
|
||||
|
||||
if (parser.isSet(importAllAndExitOption) || parser.isSet(importDesktopAndExitOption) || parser.isSet(startupAndExitOption)) {
|
||||
App *alakarteApp = App::instance();
|
||||
|
||||
QObject::connect(alakarteApp, &App::importCompleted, &app, [&app](int count) {
|
||||
QTextStream(stdout) << count << Qt::endl;
|
||||
app.exit(0);
|
||||
});
|
||||
QObject::connect(alakarteApp, &App::importError, &app, [&app](const QString &error) {
|
||||
QTextStream(stderr) << error << Qt::endl;
|
||||
app.exit(1);
|
||||
});
|
||||
|
||||
if (parser.isSet(importAllAndExitOption)) {
|
||||
alakarteApp->importAllGames();
|
||||
} else if (parser.isSet(importDesktopAndExitOption)) {
|
||||
alakarteApp->importFromDesktop();
|
||||
} else {
|
||||
if (!alakarteApp->config() || !alakarteApp->config()->autoImportOnStartup()) {
|
||||
QTextStream(stdout) << 0 << Qt::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
QTimer::singleShot(60000, &app, [&app]() {
|
||||
QTextStream(stderr) << QStringLiteral("Timed out waiting for auto-import") << Qt::endl;
|
||||
app.exit(2);
|
||||
});
|
||||
}
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
KDBusService service(KDBusService::Unique);
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
|
||||
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
|
||||
|
|
|
|||
|
|
@ -12,7 +12,18 @@ import org.kde.alakarte
|
|||
Kirigami.OverlaySheet {
|
||||
id: detailsSheet
|
||||
|
||||
property var game
|
||||
property var game: null
|
||||
property int lastNonCouchUiMode: Config.Auto
|
||||
|
||||
readonly property bool effectiveCouchMode: {
|
||||
if (App.config.uiMode === Config.Couch) return true
|
||||
if (App.config.uiMode !== Config.Auto) return false
|
||||
if (GamepadManager.connected) {
|
||||
if (InputManager.activeInput === InputManager.Gamepad) return true
|
||||
if (!InputManager.hasSeenKeyboardMouse) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
readonly property var screenshotsModel: game ? App.mediaManager.screenshotsModel(game) : null
|
||||
|
||||
|
|
@ -418,6 +429,24 @@ Kirigami.OverlaySheet {
|
|||
onClicked: detailsSheet.editRequested()
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
icon.name: "view-fullscreen"
|
||||
text: detailsSheet.effectiveCouchMode ? i18n("Couch mode: On") : i18n("Couch mode: Off")
|
||||
display: detailsSheet.useCompactLayout ? QQC2.AbstractButton.TextUnderIcon : QQC2.AbstractButton.TextBesideIcon
|
||||
onClicked: {
|
||||
if (detailsSheet.effectiveCouchMode) {
|
||||
if (detailsSheet.lastNonCouchUiMode === Config.Auto) {
|
||||
App.config.uiMode = Config.Desktop
|
||||
} else {
|
||||
App.config.uiMode = detailsSheet.lastNonCouchUiMode
|
||||
}
|
||||
} else {
|
||||
detailsSheet.lastNonCouchUiMode = App.config.uiMode
|
||||
App.config.uiMode = Config.Couch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
icon.name: "dialog-information"
|
||||
text: i18n("Diagnostics")
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ Kirigami.Dialog {
|
|||
|
||||
property var game: null
|
||||
property bool isEditing: game !== null
|
||||
property string editProfileId: "default"
|
||||
|
||||
ListModel {
|
||||
id: envModel
|
||||
|
|
@ -49,10 +50,19 @@ Kirigami.Dialog {
|
|||
game.developer = developerField.text.trim()
|
||||
game.launchCommand = executableField.text.trim()
|
||||
game.workingDirectory = workingDirField.text.trim()
|
||||
game.launchEnv = dialog.envModelToMap()
|
||||
game.launchRunner = runnerValue
|
||||
game.launchRunnerPath = runnerPathValue
|
||||
game.launchPrefixPath = prefixPathValue
|
||||
if (dialog.editProfileId === "couch") {
|
||||
game.setLaunchProfileConfig("couch", {
|
||||
"runner": runnerValue,
|
||||
"runnerPath": runnerPathValue,
|
||||
"prefixPath": prefixPathValue,
|
||||
"env": dialog.envModelToMap()
|
||||
})
|
||||
} else {
|
||||
game.launchEnv = dialog.envModelToMap()
|
||||
game.launchRunner = runnerValue
|
||||
game.launchRunnerPath = runnerPathValue
|
||||
game.launchPrefixPath = prefixPathValue
|
||||
}
|
||||
if (selectedCoverPath !== "") {
|
||||
App.setCoverFromFile(game, selectedCoverPath)
|
||||
}
|
||||
|
|
@ -86,12 +96,15 @@ Kirigami.Dialog {
|
|||
|
||||
property string selectedCoverPath: ""
|
||||
|
||||
readonly property bool anyMenuOpen: runnerCombo && runnerCombo.popup && runnerCombo.popup.visible
|
||||
readonly property bool anyMenuOpen: (runnerCombo && runnerCombo.popup && runnerCombo.popup.visible) || (profileCombo && profileCombo.popup && profileCombo.popup.visible)
|
||||
|
||||
function closeCurrentMenu() {
|
||||
if (runnerCombo && runnerCombo.popup && runnerCombo.popup.visible) {
|
||||
runnerCombo.popup.close()
|
||||
}
|
||||
if (profileCombo && profileCombo.popup && profileCombo.popup.visible) {
|
||||
profileCombo.popup.close()
|
||||
}
|
||||
}
|
||||
|
||||
function isDescendant(item, ancestor) {
|
||||
|
|
@ -181,35 +194,77 @@ Kirigami.Dialog {
|
|||
return 0
|
||||
}
|
||||
|
||||
function loadFields() {
|
||||
selectedCoverPath = ""
|
||||
function profileIdFromIndex(idx) {
|
||||
if (idx === 1) return "couch"
|
||||
return "default"
|
||||
}
|
||||
|
||||
function profileIndexFromId(profileId) {
|
||||
if ((profileId || "").trim() === "couch") return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
function loadProfileFields() {
|
||||
envModel.clear()
|
||||
if (isEditing && game) {
|
||||
nameField.text = game.name || ""
|
||||
developerField.text = game.developer || ""
|
||||
executableField.text = game.launchCommand || ""
|
||||
workingDirField.text = game.workingDirectory || ""
|
||||
|
||||
runnerCombo.currentIndex = dialog.runnerToIndex(game.launchRunner)
|
||||
runnerPathField.text = game.launchRunnerPath || ""
|
||||
prefixPathField.text = game.launchPrefixPath || ""
|
||||
if (!isEditing || !game) {
|
||||
runnerCombo.currentIndex = 0
|
||||
runnerPathField.text = ""
|
||||
prefixPathField.text = ""
|
||||
return
|
||||
}
|
||||
|
||||
let env = game.launchEnv || ({})
|
||||
if (dialog.editProfileId === "couch") {
|
||||
let cfg = game.launchProfileConfig("couch") || ({})
|
||||
runnerCombo.currentIndex = dialog.runnerToIndex(cfg.runner)
|
||||
runnerPathField.text = cfg.runnerPath || ""
|
||||
prefixPathField.text = cfg.prefixPath || ""
|
||||
let env = cfg.env || ({})
|
||||
let keys = Object.keys(env)
|
||||
keys.sort()
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let k = keys[i]
|
||||
envModel.append({ key: k, value: String(env[k]) })
|
||||
}
|
||||
} else {
|
||||
runnerCombo.currentIndex = dialog.runnerToIndex(game.launchRunner)
|
||||
runnerPathField.text = game.launchRunnerPath || ""
|
||||
prefixPathField.text = game.launchPrefixPath || ""
|
||||
let env = game.launchEnv || ({})
|
||||
let keys = Object.keys(env)
|
||||
keys.sort()
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let k = keys[i]
|
||||
envModel.append({ key: k, value: String(env[k]) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadFields() {
|
||||
selectedCoverPath = ""
|
||||
if (isEditing && game) {
|
||||
nameField.text = game.name || ""
|
||||
developerField.text = game.developer || ""
|
||||
executableField.text = game.launchCommand || ""
|
||||
workingDirField.text = game.workingDirectory || ""
|
||||
|
||||
dialog.editProfileId = (App.config.uiMode === Config.Couch
|
||||
|| (App.config.uiMode === Config.Auto
|
||||
&& GamepadManager.connected
|
||||
&& (InputManager.activeInput === InputManager.Gamepad || !InputManager.hasSeenKeyboardMouse)))
|
||||
? "couch"
|
||||
: "default"
|
||||
profileCombo.currentIndex = dialog.profileIndexFromId(dialog.editProfileId)
|
||||
dialog.loadProfileFields()
|
||||
} else {
|
||||
nameField.text = ""
|
||||
developerField.text = ""
|
||||
executableField.text = ""
|
||||
workingDirField.text = ""
|
||||
|
||||
runnerCombo.currentIndex = 0
|
||||
runnerPathField.text = ""
|
||||
prefixPathField.text = ""
|
||||
dialog.editProfileId = "default"
|
||||
profileCombo.currentIndex = 0
|
||||
dialog.loadProfileFields()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -325,6 +380,35 @@ Kirigami.Dialog {
|
|||
title: i18n("Compatibility")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
visible: isEditing
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
QQC2.Label {
|
||||
text: i18n("Profile")
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
QQC2.ComboBox {
|
||||
id: profileCombo
|
||||
Layout.fillWidth: true
|
||||
model: [i18n("Default"), i18n("Couch")]
|
||||
onCurrentIndexChanged: if (dialog.isEditing) {
|
||||
dialog.editProfileId = dialog.profileIdFromIndex(currentIndex)
|
||||
dialog.loadProfileFields()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator { visible: isEditing }
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
|
|
|
|||
|
|
@ -1183,7 +1183,7 @@ Kirigami.ApplicationWindow {
|
|||
secondary: "steam"
|
||||
resourceFallback: Qt.resolvedUrl("icons/brand/steam-symbolic.svg")
|
||||
}
|
||||
enabled: !App.importing && App.config.importSteam && !App.gameModel.hasPlatformPrefix("Steam") && App.gameModel.count >= 0
|
||||
enabled: !App.importing && App.config.importSteam && App.gameModel.count >= 0
|
||||
onClicked: App.importFromSteam()
|
||||
}
|
||||
|
||||
|
|
@ -1197,7 +1197,7 @@ Kirigami.ApplicationWindow {
|
|||
primary: "lutris"
|
||||
secondary: "applications-games"
|
||||
}
|
||||
enabled: !App.importing && App.config.importLutris && !App.gameModel.hasPlatformPrefix("Lutris") && App.gameModel.count >= 0
|
||||
enabled: !App.importing && App.config.importLutris && App.gameModel.count >= 0
|
||||
onClicked: App.importFromLutris()
|
||||
}
|
||||
|
||||
|
|
@ -1211,7 +1211,7 @@ Kirigami.ApplicationWindow {
|
|||
primary: "com.heroicgameslauncher.hgl"
|
||||
secondary: "applications-games"
|
||||
}
|
||||
enabled: !App.importing && App.config.importHeroic && !App.gameModel.hasPlatformPrefix("Heroic") && App.gameModel.count >= 0
|
||||
enabled: !App.importing && App.config.importHeroic && App.gameModel.count >= 0
|
||||
onClicked: App.importFromHeroic()
|
||||
}
|
||||
|
||||
|
|
@ -1225,7 +1225,7 @@ Kirigami.ApplicationWindow {
|
|||
primary: "user-desktop"
|
||||
secondary: "computer"
|
||||
}
|
||||
enabled: !App.importing && App.config.importDesktop && !App.gameModel.hasPlatformPrefix("Desktop") && App.gameModel.count >= 0
|
||||
enabled: !App.importing && App.config.importDesktop && App.gameModel.count >= 0
|
||||
onClicked: App.importFromDesktop()
|
||||
}
|
||||
|
||||
|
|
@ -1239,7 +1239,7 @@ Kirigami.ApplicationWindow {
|
|||
primary: "com.usebottles.bottles"
|
||||
secondary: "application-x-executable"
|
||||
}
|
||||
enabled: !App.importing && App.config.importBottles && !App.gameModel.hasPlatformPrefix("Bottles") && App.gameModel.count >= 0
|
||||
enabled: !App.importing && App.config.importBottles && App.gameModel.count >= 0
|
||||
onClicked: App.importFromBottles()
|
||||
}
|
||||
|
||||
|
|
@ -1253,7 +1253,7 @@ Kirigami.ApplicationWindow {
|
|||
primary: "flatpak-discover"
|
||||
secondary: "applications-games"
|
||||
}
|
||||
enabled: !App.importing && App.config.importFlatpak && !App.gameModel.hasPlatformPrefix("Flatpak") && App.gameModel.count >= 0
|
||||
enabled: !App.importing && App.config.importFlatpak && App.gameModel.count >= 0
|
||||
onClicked: App.importFromFlatpak()
|
||||
}
|
||||
|
||||
|
|
@ -1268,7 +1268,7 @@ Kirigami.ApplicationWindow {
|
|||
secondary: "itch"
|
||||
resourceFallback: Qt.resolvedUrl("icons/brand/itchdotio-symbolic.svg")
|
||||
}
|
||||
enabled: !App.importing && App.config.importItch && !App.gameModel.hasPlatformPrefix("itch.io") && App.gameModel.count >= 0
|
||||
enabled: !App.importing && App.config.importItch && App.gameModel.count >= 0
|
||||
onClicked: App.importFromItch()
|
||||
}
|
||||
|
||||
|
|
@ -1282,7 +1282,7 @@ Kirigami.ApplicationWindow {
|
|||
primary: "legendary"
|
||||
secondary: "applications-games"
|
||||
}
|
||||
enabled: !App.importing && App.config.importLegendary && !App.gameModel.hasPlatformPrefix("Legendary") && App.gameModel.count >= 0
|
||||
enabled: !App.importing && App.config.importLegendary && App.gameModel.count >= 0
|
||||
onClicked: App.importFromLegendary()
|
||||
}
|
||||
|
||||
|
|
@ -1297,7 +1297,7 @@ Kirigami.ApplicationWindow {
|
|||
secondary: "retroarch"
|
||||
resourceFallback: Qt.resolvedUrl("icons/brand/retroarch-symbolic.svg")
|
||||
}
|
||||
enabled: !App.importing && App.config.importRetroArch && !App.gameModel.hasPlatformPrefix("RetroArch") && App.gameModel.count >= 0
|
||||
enabled: !App.importing && App.config.importRetroArch && App.gameModel.count >= 0
|
||||
onClicked: App.importFromRetroArch()
|
||||
}
|
||||
}
|
||||
|
|
@ -1309,15 +1309,15 @@ Kirigami.ApplicationWindow {
|
|||
QQC2.Button {
|
||||
text: i18n("Import All")
|
||||
icon.name: "document-import"
|
||||
enabled: !App.importing && App.gameModel.count >= 0 && ((App.config.importSteam && !App.gameModel.hasPlatformPrefix("Steam"))
|
||||
|| (App.config.importLutris && !App.gameModel.hasPlatformPrefix("Lutris"))
|
||||
|| (App.config.importHeroic && !App.gameModel.hasPlatformPrefix("Heroic"))
|
||||
|| (App.config.importDesktop && !App.gameModel.hasPlatformPrefix("Desktop"))
|
||||
|| (App.config.importBottles && !App.gameModel.hasPlatformPrefix("Bottles"))
|
||||
|| (App.config.importFlatpak && !App.gameModel.hasPlatformPrefix("Flatpak"))
|
||||
|| (App.config.importItch && !App.gameModel.hasPlatformPrefix("itch.io"))
|
||||
|| (App.config.importLegendary && !App.gameModel.hasPlatformPrefix("Legendary"))
|
||||
|| (App.config.importRetroArch && !App.gameModel.hasPlatformPrefix("RetroArch")))
|
||||
enabled: !App.importing && App.gameModel.count >= 0 && (App.config.importSteam
|
||||
|| App.config.importLutris
|
||||
|| App.config.importHeroic
|
||||
|| App.config.importDesktop
|
||||
|| App.config.importBottles
|
||||
|| App.config.importFlatpak
|
||||
|| App.config.importItch
|
||||
|| App.config.importLegendary
|
||||
|| App.config.importRetroArch)
|
||||
onClicked: App.importAllGames()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -643,15 +643,15 @@ FormCard.FormHeader {
|
|||
text: i18n("Import All Games")
|
||||
description: i18n("Scan all enabled sources")
|
||||
icon.name: "document-import"
|
||||
enabled: !App.importing && App.gameModel.count >= 0 && ((App.config.importSteam && !App.gameModel.hasPlatformPrefix("Steam"))
|
||||
|| (App.config.importLutris && !App.gameModel.hasPlatformPrefix("Lutris"))
|
||||
|| (App.config.importHeroic && !App.gameModel.hasPlatformPrefix("Heroic"))
|
||||
|| (App.config.importDesktop && !App.gameModel.hasPlatformPrefix("Desktop"))
|
||||
|| (App.config.importBottles && !App.gameModel.hasPlatformPrefix("Bottles"))
|
||||
|| (App.config.importFlatpak && !App.gameModel.hasPlatformPrefix("Flatpak"))
|
||||
|| (App.config.importItch && !App.gameModel.hasPlatformPrefix("itch.io"))
|
||||
|| (App.config.importLegendary && !App.gameModel.hasPlatformPrefix("Legendary"))
|
||||
|| (App.config.importRetroArch && !App.gameModel.hasPlatformPrefix("RetroArch")))
|
||||
enabled: !App.importing && App.gameModel.count >= 0 && (App.config.importSteam
|
||||
|| App.config.importLutris
|
||||
|| App.config.importHeroic
|
||||
|| App.config.importDesktop
|
||||
|| App.config.importBottles
|
||||
|| App.config.importFlatpak
|
||||
|| App.config.importItch
|
||||
|| App.config.importLegendary
|
||||
|| App.config.importRetroArch)
|
||||
onClicked: App.importAllGames()
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue