mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-02-09 13:03:09 +00:00
Core: per-game compatibility and env overrides
This commit is contained in:
parent
49997ed0cb
commit
99406a1989
4 changed files with 637 additions and 10 deletions
149
src/game.cpp
149
src/game.cpp
|
|
@ -4,6 +4,8 @@
|
|||
#include "game.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <QCryptographicHash>
|
||||
#include <QJsonValue>
|
||||
#include <QLocale>
|
||||
#include <QUuid>
|
||||
|
||||
|
|
@ -131,6 +133,58 @@ void Game::setWorkingDirectory(const QString &dir)
|
|||
}
|
||||
}
|
||||
|
||||
QVariantMap Game::launchEnv() const
|
||||
{
|
||||
return m_launchEnv;
|
||||
}
|
||||
|
||||
void Game::setLaunchEnv(const QVariantMap &env)
|
||||
{
|
||||
if (m_launchEnv != env) {
|
||||
m_launchEnv = env;
|
||||
Q_EMIT launchEnvChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QString Game::launchRunner() const
|
||||
{
|
||||
return m_launchRunner;
|
||||
}
|
||||
|
||||
void Game::setLaunchRunner(const QString &runner)
|
||||
{
|
||||
if (m_launchRunner != runner) {
|
||||
m_launchRunner = runner;
|
||||
Q_EMIT launchRunnerChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QString Game::launchRunnerPath() const
|
||||
{
|
||||
return m_launchRunnerPath;
|
||||
}
|
||||
|
||||
void Game::setLaunchRunnerPath(const QString &path)
|
||||
{
|
||||
if (m_launchRunnerPath != path) {
|
||||
m_launchRunnerPath = path;
|
||||
Q_EMIT launchRunnerPathChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QString Game::launchPrefixPath() const
|
||||
{
|
||||
return m_launchPrefixPath;
|
||||
}
|
||||
|
||||
void Game::setLaunchPrefixPath(const QString &path)
|
||||
{
|
||||
if (m_launchPrefixPath != path) {
|
||||
m_launchPrefixPath = path;
|
||||
Q_EMIT launchPrefixPathChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QString Game::platform() const
|
||||
{
|
||||
return m_platform;
|
||||
|
|
@ -268,6 +322,32 @@ QJsonObject Game::toJson() const
|
|||
obj[QStringLiteral("favorite")] = m_favorite;
|
||||
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();
|
||||
if (hasLaunchConfig) {
|
||||
QJsonObject launchObj;
|
||||
|
||||
if (!m_launchEnv.isEmpty()) {
|
||||
QJsonObject envObj;
|
||||
for (auto it = m_launchEnv.constBegin(); it != m_launchEnv.constEnd(); ++it) {
|
||||
envObj.insert(it.key(), QJsonValue::fromVariant(it.value()));
|
||||
}
|
||||
launchObj.insert(QStringLiteral("env"), envObj);
|
||||
}
|
||||
|
||||
if (!m_launchRunner.isEmpty()) {
|
||||
launchObj.insert(QStringLiteral("runner"), m_launchRunner);
|
||||
}
|
||||
if (!m_launchRunnerPath.isEmpty()) {
|
||||
launchObj.insert(QStringLiteral("runnerPath"), m_launchRunnerPath);
|
||||
}
|
||||
if (!m_launchPrefixPath.isEmpty()) {
|
||||
launchObj.insert(QStringLiteral("prefixPath"), m_launchPrefixPath);
|
||||
}
|
||||
|
||||
obj.insert(QStringLiteral("launch"), launchObj);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
|
@ -276,26 +356,85 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
|
|||
QString id = json[QStringLiteral("id")].toString();
|
||||
QString name = json[QStringLiteral("name")].toString();
|
||||
|
||||
if (id.isEmpty() || name.isEmpty()) {
|
||||
if (name.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const QString platformId = json[QStringLiteral("platformId")].toString();
|
||||
const QString platform = json[QStringLiteral("platform")].toString();
|
||||
const QString launchCommand = json[QStringLiteral("launchCommand")].toString();
|
||||
const QString workingDirectory = json[QStringLiteral("workingDirectory")].toString();
|
||||
if (id.isEmpty()) {
|
||||
const QByteArray fingerprint =
|
||||
(platformId + QLatin1Char('|') + platform + QLatin1Char('|') + name + QLatin1Char('|') + launchCommand + QLatin1Char('|') + workingDirectory)
|
||||
.toUtf8();
|
||||
const QByteArray hash = QCryptographicHash::hash(fingerprint, QCryptographicHash::Sha1).toHex();
|
||||
id = QStringLiteral("legacy-") + QString::fromLatin1(hash);
|
||||
}
|
||||
|
||||
Game *game = new Game(id, name, parent);
|
||||
game->setDescription(json[QStringLiteral("description")].toString());
|
||||
game->setDeveloper(json[QStringLiteral("developer")].toString());
|
||||
game->setPublisher(json[QStringLiteral("publisher")].toString());
|
||||
game->setCoverUrl(QUrl(json[QStringLiteral("coverUrl")].toString()));
|
||||
game->setIconUrl(QUrl(json[QStringLiteral("iconUrl")].toString()));
|
||||
game->setLaunchCommand(json[QStringLiteral("launchCommand")].toString());
|
||||
game->setWorkingDirectory(json[QStringLiteral("workingDirectory")].toString());
|
||||
game->setPlatform(json[QStringLiteral("platform")].toString());
|
||||
game->setPlatformId(json[QStringLiteral("platformId")].toString());
|
||||
game->setLaunchCommand(launchCommand);
|
||||
game->setWorkingDirectory(workingDirectory);
|
||||
game->setPlatform(platform);
|
||||
game->setPlatformId(platformId);
|
||||
game->setDateAdded(QDateTime::fromString(json[QStringLiteral("dateAdded")].toString(), Qt::ISODate));
|
||||
game->setLastPlayed(QDateTime::fromString(json[QStringLiteral("lastPlayed")].toString(), Qt::ISODate));
|
||||
game->setPlayTime(json[QStringLiteral("playTime")].toInteger());
|
||||
game->setFavorite(json[QStringLiteral("favorite")].toBool());
|
||||
game->setHidden(json[QStringLiteral("hidden")].toBool());
|
||||
game->setInstalled(json[QStringLiteral("installed")].toBool(true));
|
||||
|
||||
QVariantMap env;
|
||||
QString runner;
|
||||
QString runnerPath;
|
||||
QString prefixPath;
|
||||
const QJsonValue launchValue = json.value(QStringLiteral("launch"));
|
||||
if (launchValue.isObject()) {
|
||||
const QJsonObject launchObj = launchValue.toObject();
|
||||
const QJsonValue envValue = launchObj.value(QStringLiteral("env"));
|
||||
if (envValue.isObject()) {
|
||||
env = envValue.toObject().toVariantMap();
|
||||
}
|
||||
|
||||
runner = launchObj.value(QStringLiteral("runner")).toString();
|
||||
runnerPath = launchObj.value(QStringLiteral("runnerPath")).toString();
|
||||
prefixPath = launchObj.value(QStringLiteral("prefixPath")).toString();
|
||||
}
|
||||
|
||||
const QJsonValue legacyEnvValue = json.value(QStringLiteral("launchEnv"));
|
||||
if (env.isEmpty() && legacyEnvValue.isObject()) {
|
||||
env = legacyEnvValue.toObject().toVariantMap();
|
||||
}
|
||||
|
||||
if (runner.isEmpty()) {
|
||||
runner = json.value(QStringLiteral("launchRunner")).toString();
|
||||
}
|
||||
if (runnerPath.isEmpty()) {
|
||||
runnerPath = json.value(QStringLiteral("launchRunnerPath")).toString();
|
||||
}
|
||||
if (prefixPath.isEmpty()) {
|
||||
prefixPath = json.value(QStringLiteral("launchPrefixPath")).toString();
|
||||
}
|
||||
|
||||
if (!env.isEmpty()) {
|
||||
game->setLaunchEnv(env);
|
||||
}
|
||||
|
||||
if (!runner.isEmpty()) {
|
||||
game->setLaunchRunner(runner);
|
||||
}
|
||||
if (!runnerPath.isEmpty()) {
|
||||
game->setLaunchRunnerPath(runnerPath);
|
||||
}
|
||||
if (!prefixPath.isEmpty()) {
|
||||
game->setLaunchPrefixPath(prefixPath);
|
||||
}
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
|
|
|
|||
25
src/game.h
25
src/game.h
|
|
@ -9,6 +9,7 @@
|
|||
#include <QQmlEngine>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QVariantMap>
|
||||
|
||||
class Game : public QObject
|
||||
{
|
||||
|
|
@ -25,6 +26,10 @@ class Game : public QObject
|
|||
Q_PROPERTY(QUrl iconUrl READ iconUrl WRITE setIconUrl NOTIFY iconUrlChanged)
|
||||
Q_PROPERTY(QString launchCommand READ launchCommand WRITE setLaunchCommand NOTIFY launchCommandChanged)
|
||||
Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged)
|
||||
Q_PROPERTY(QVariantMap launchEnv READ launchEnv WRITE setLaunchEnv NOTIFY launchEnvChanged)
|
||||
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 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)
|
||||
|
|
@ -65,6 +70,18 @@ public:
|
|||
QString workingDirectory() const;
|
||||
void setWorkingDirectory(const QString &dir);
|
||||
|
||||
QVariantMap launchEnv() const;
|
||||
void setLaunchEnv(const QVariantMap &env);
|
||||
|
||||
QString launchRunner() const;
|
||||
void setLaunchRunner(const QString &runner);
|
||||
|
||||
QString launchRunnerPath() const;
|
||||
void setLaunchRunnerPath(const QString &path);
|
||||
|
||||
QString launchPrefixPath() const;
|
||||
void setLaunchPrefixPath(const QString &path);
|
||||
|
||||
QString platform() const;
|
||||
void setPlatform(const QString &platform);
|
||||
|
||||
|
|
@ -108,6 +125,10 @@ Q_SIGNALS:
|
|||
void iconUrlChanged();
|
||||
void launchCommandChanged();
|
||||
void workingDirectoryChanged();
|
||||
void launchEnvChanged();
|
||||
void launchRunnerChanged();
|
||||
void launchRunnerPathChanged();
|
||||
void launchPrefixPathChanged();
|
||||
void platformChanged();
|
||||
void platformIdChanged();
|
||||
void dateAddedChanged();
|
||||
|
|
@ -128,6 +149,10 @@ private:
|
|||
QUrl m_iconUrl;
|
||||
QString m_launchCommand;
|
||||
QString m_workingDirectory;
|
||||
QVariantMap m_launchEnv;
|
||||
QString m_launchRunner;
|
||||
QString m_launchRunnerPath;
|
||||
QString m_launchPrefixPath;
|
||||
QString m_platform;
|
||||
QString m_platformId;
|
||||
QDateTime m_dateAdded;
|
||||
|
|
|
|||
|
|
@ -7,9 +7,107 @@
|
|||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
static QStringList steamCandidateRoots()
|
||||
{
|
||||
const QString home = QDir::homePath();
|
||||
return {
|
||||
home + QStringLiteral("/.steam/root"),
|
||||
home + QStringLiteral("/.steam/steam"),
|
||||
home + QStringLiteral("/.local/share/Steam"),
|
||||
home + QStringLiteral("/.var/app/com.valvesoftware.Steam/data/Steam"),
|
||||
home + QStringLiteral("/.var/app/com.valvesoftware.Steam/.local/share/Steam"),
|
||||
};
|
||||
}
|
||||
|
||||
static QString findSteamClientInstallPathFromProton(const QString &protonExe)
|
||||
{
|
||||
if (protonExe.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QDir dir(QFileInfo(protonExe).absoluteDir());
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (dir.exists(QStringLiteral("steamapps"))) {
|
||||
return dir.absolutePath();
|
||||
}
|
||||
if (!dir.cdUp()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &root : steamCandidateRoots()) {
|
||||
if (QDir(root).exists(QStringLiteral("steamapps"))) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static QString discoverDefaultProtonExecutable()
|
||||
{
|
||||
static bool done = false;
|
||||
static QString cached;
|
||||
|
||||
if (done) {
|
||||
return cached;
|
||||
}
|
||||
done = true;
|
||||
|
||||
QStringList candidates;
|
||||
for (const QString &root : steamCandidateRoots()) {
|
||||
const QString compatTools = root + QStringLiteral("/compatibilitytools.d");
|
||||
QDir compatDir(compatTools);
|
||||
if (compatDir.exists()) {
|
||||
const QStringList toolDirs = compatDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
for (const QString &tool : toolDirs) {
|
||||
const QString protonPath = compatTools + QLatin1Char('/') + tool + QStringLiteral("/proton");
|
||||
if (QFileInfo::exists(protonPath)) {
|
||||
candidates.append(protonPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const QString common = root + QStringLiteral("/steamapps/common");
|
||||
QDir commonDir(common);
|
||||
if (commonDir.exists()) {
|
||||
const QStringList dirs = commonDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
for (const QString &d : dirs) {
|
||||
if (!d.contains(QStringLiteral("Proton"), Qt::CaseInsensitive)) {
|
||||
continue;
|
||||
}
|
||||
const QString protonPath = common + QLatin1Char('/') + d + QStringLiteral("/proton");
|
||||
if (QFileInfo::exists(protonPath)) {
|
||||
candidates.append(protonPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
candidates.removeDuplicates();
|
||||
candidates.sort();
|
||||
|
||||
for (const QString &c : candidates) {
|
||||
if (c.contains(QStringLiteral("Proton - Experimental"))) {
|
||||
cached = c;
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
if (!candidates.isEmpty()) {
|
||||
cached = candidates.constLast();
|
||||
}
|
||||
|
||||
return cached;
|
||||
}
|
||||
|
||||
GameLauncher::GameLauncher(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
|
|
@ -46,6 +144,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 bool hasLaunchOverrides = !runner.isEmpty() || !runnerPath.isEmpty() || !prefixPath.isEmpty() || !launchEnv.isEmpty();
|
||||
|
||||
// Check if already running
|
||||
if (m_runningGames.contains(game->id())) {
|
||||
Q_EMIT gameError(game, tr("Game is already running"));
|
||||
|
|
@ -53,7 +157,7 @@ void GameLauncher::launchGame(Game *game)
|
|||
}
|
||||
|
||||
// Handle Steam URLs
|
||||
if (command.startsWith(QLatin1String("steam://"))) {
|
||||
if (!hasLaunchOverrides && command.startsWith(QLatin1String("steam://"))) {
|
||||
QDesktopServices::openUrl(QUrl(command));
|
||||
game->setLastPlayed(QDateTime::currentDateTime());
|
||||
Q_EMIT gameStarted(game);
|
||||
|
|
@ -61,7 +165,7 @@ void GameLauncher::launchGame(Game *game)
|
|||
}
|
||||
|
||||
// Handle Lutris URLs
|
||||
if (command.startsWith(QLatin1String("lutris "))) {
|
||||
if (!hasLaunchOverrides && command.startsWith(QLatin1String("lutris "))) {
|
||||
QString lutrisCommand = command.mid(7); // Remove "lutris "
|
||||
QDesktopServices::openUrl(QUrl(lutrisCommand));
|
||||
game->setLastPlayed(QDateTime::currentDateTime());
|
||||
|
|
@ -72,6 +176,21 @@ void GameLauncher::launchGame(Game *game)
|
|||
// Start process for other commands
|
||||
QProcess *process = new QProcess(this);
|
||||
|
||||
// Apply per-game environment variables
|
||||
QProcessEnvironment env;
|
||||
bool envChanged = false;
|
||||
if (!launchEnv.isEmpty()) {
|
||||
env = QProcessEnvironment::systemEnvironment();
|
||||
envChanged = true;
|
||||
for (auto it = launchEnv.constBegin(); it != launchEnv.constEnd(); ++it) {
|
||||
const QString key = it.key();
|
||||
if (key.isEmpty() || key.contains(QLatin1Char('='))) {
|
||||
continue;
|
||||
}
|
||||
env.insert(key, it.value().toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Set working directory if available
|
||||
if (!game->workingDirectory().isEmpty()) {
|
||||
process->setWorkingDirectory(game->workingDirectory());
|
||||
|
|
@ -84,7 +203,14 @@ void GameLauncher::launchGame(Game *game)
|
|||
m_processToGame.insert(process, game);
|
||||
|
||||
// Parse command and arguments
|
||||
QStringList parts = QProcess::splitCommand(command);
|
||||
QStringList parts;
|
||||
if (command.startsWith(QLatin1String("steam://"))) {
|
||||
parts = {QStringLiteral("xdg-open"), command};
|
||||
} else if (command.startsWith(QLatin1String("lutris "))) {
|
||||
parts = {QStringLiteral("xdg-open"), command.mid(7)};
|
||||
} else {
|
||||
parts = QProcess::splitCommand(command);
|
||||
}
|
||||
if (parts.isEmpty()) {
|
||||
cleanupProcess(process);
|
||||
Q_EMIT gameError(game, tr("Invalid launch command"));
|
||||
|
|
@ -92,7 +218,89 @@ void GameLauncher::launchGame(Game *game)
|
|||
}
|
||||
|
||||
QString program = parts.takeFirst();
|
||||
process->start(program, parts);
|
||||
|
||||
QString finalProgram = program;
|
||||
QStringList finalArgs = parts;
|
||||
|
||||
if ((runner == QLatin1String("wine") || runner == QLatin1String("proton")) && (program == QLatin1String("xdg-open"))) {
|
||||
cleanupProcess(process);
|
||||
Q_EMIT gameError(game, tr("This runner cannot be used with URL-based launch commands"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (runner == QLatin1String("custom")) {
|
||||
if (runnerPath.isEmpty()) {
|
||||
cleanupProcess(process);
|
||||
Q_EMIT gameError(game, tr("Custom runner is enabled but no runner path is configured"));
|
||||
return;
|
||||
}
|
||||
finalProgram = runnerPath;
|
||||
finalArgs = {program};
|
||||
finalArgs.append(parts);
|
||||
} else if (runner == QLatin1String("wine")) {
|
||||
QString wineExe = runnerPath;
|
||||
if (wineExe.isEmpty()) {
|
||||
wineExe = QStandardPaths::findExecutable(QStringLiteral("wine"));
|
||||
}
|
||||
if (wineExe.isEmpty()) {
|
||||
cleanupProcess(process);
|
||||
Q_EMIT gameError(game, tr("Wine runner is enabled but Wine was not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
QString winePrefix = prefixPath;
|
||||
if (winePrefix.isEmpty()) {
|
||||
winePrefix = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/prefixes/") + game->id();
|
||||
}
|
||||
QDir().mkpath(winePrefix);
|
||||
|
||||
if (!envChanged) {
|
||||
env = QProcessEnvironment::systemEnvironment();
|
||||
envChanged = true;
|
||||
}
|
||||
env.insert(QStringLiteral("WINEPREFIX"), winePrefix);
|
||||
|
||||
finalProgram = wineExe;
|
||||
finalArgs = {program};
|
||||
finalArgs.append(parts);
|
||||
} else if (runner == QLatin1String("proton")) {
|
||||
QString protonExe = runnerPath;
|
||||
if (protonExe.isEmpty()) {
|
||||
protonExe = discoverDefaultProtonExecutable();
|
||||
}
|
||||
if (protonExe.isEmpty()) {
|
||||
cleanupProcess(process);
|
||||
Q_EMIT gameError(game, tr("Proton runner is enabled but no Proton installation was found"));
|
||||
return;
|
||||
}
|
||||
|
||||
QString steamInstallPath = findSteamClientInstallPathFromProton(protonExe);
|
||||
|
||||
QString compatDataPath = prefixPath;
|
||||
if (compatDataPath.isEmpty()) {
|
||||
compatDataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/prefixes/") + game->id();
|
||||
}
|
||||
QDir().mkpath(compatDataPath);
|
||||
|
||||
if (!envChanged) {
|
||||
env = QProcessEnvironment::systemEnvironment();
|
||||
envChanged = true;
|
||||
}
|
||||
env.insert(QStringLiteral("STEAM_COMPAT_DATA_PATH"), compatDataPath);
|
||||
if (!steamInstallPath.isEmpty()) {
|
||||
env.insert(QStringLiteral("STEAM_COMPAT_CLIENT_INSTALL_PATH"), steamInstallPath);
|
||||
}
|
||||
|
||||
finalProgram = protonExe;
|
||||
finalArgs = {QStringLiteral("run"), program};
|
||||
finalArgs.append(parts);
|
||||
}
|
||||
|
||||
if (envChanged) {
|
||||
process->setProcessEnvironment(env);
|
||||
}
|
||||
|
||||
process->start(finalProgram, finalArgs);
|
||||
|
||||
if (!process->waitForStarted(5000)) {
|
||||
QString error = process->errorString();
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ Kirigami.Dialog {
|
|||
property var game: null
|
||||
property bool isEditing: game !== null
|
||||
|
||||
ListModel {
|
||||
id: envModel
|
||||
}
|
||||
|
||||
signal gameCreated(var game)
|
||||
signal gameUpdated(var game)
|
||||
|
||||
|
|
@ -31,13 +35,24 @@ Kirigami.Dialog {
|
|||
Kirigami.Action {
|
||||
text: isEditing ? i18n("Apply") : i18n("Add")
|
||||
icon.name: isEditing ? "dialog-ok-apply" : "list-add"
|
||||
enabled: nameField.text.trim() !== "" && executableField.text.trim() !== ""
|
||||
enabled: nameField.text.trim() !== "" && executableField.text.trim() !== "" && (runnerCombo.currentIndex !== 3 || runnerPathField.text.trim() !== "")
|
||||
onTriggered: {
|
||||
let runnerValue = dialog.runnerFromIndex(runnerCombo.currentIndex)
|
||||
let runnerPathValue = runnerPathField.text.trim()
|
||||
let prefixPathValue = prefixPathField.text.trim()
|
||||
if (runnerValue === "") {
|
||||
runnerPathValue = ""
|
||||
prefixPathValue = ""
|
||||
}
|
||||
if (isEditing) {
|
||||
game.name = nameField.text.trim()
|
||||
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 (selectedCoverPath !== "") {
|
||||
App.setCoverFromFile(game, selectedCoverPath)
|
||||
}
|
||||
|
|
@ -48,6 +63,10 @@ Kirigami.Dialog {
|
|||
if (newGame) {
|
||||
newGame.developer = developerField.text.trim()
|
||||
newGame.workingDirectory = workingDirField.text.trim()
|
||||
newGame.launchEnv = dialog.envModelToMap()
|
||||
newGame.launchRunner = runnerValue
|
||||
newGame.launchRunnerPath = runnerPathValue
|
||||
newGame.launchPrefixPath = prefixPathValue
|
||||
if (selectedCoverPath !== "") {
|
||||
App.setCoverFromFile(newGame, selectedCoverPath)
|
||||
}
|
||||
|
|
@ -109,18 +128,75 @@ Kirigami.Dialog {
|
|||
}
|
||||
}
|
||||
|
||||
function envModelToMap() {
|
||||
let result = ({})
|
||||
for (let i = 0; i < envModel.count; i++) {
|
||||
let row = envModel.get(i)
|
||||
let k = (row.key || "").trim()
|
||||
if (k === "") continue
|
||||
if (k.indexOf("=") !== -1) continue
|
||||
result[k] = String(row.value || "")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function setEnvVar(key, value) {
|
||||
let k = (key || "").trim()
|
||||
if (k === "") return
|
||||
for (let i = 0; i < envModel.count; i++) {
|
||||
let row = envModel.get(i)
|
||||
if ((row.key || "").trim() === k) {
|
||||
envModel.setProperty(i, "value", String(value || ""))
|
||||
return
|
||||
}
|
||||
}
|
||||
envModel.append({ key: k, value: String(value || "") })
|
||||
}
|
||||
|
||||
function runnerFromIndex(idx) {
|
||||
if (idx === 1) return "wine"
|
||||
if (idx === 2) return "proton"
|
||||
if (idx === 3) return "custom"
|
||||
return ""
|
||||
}
|
||||
|
||||
function runnerToIndex(runner) {
|
||||
let r = (runner || "").trim().toLowerCase()
|
||||
if (r === "wine") return 1
|
||||
if (r === "proton") return 2
|
||||
if (r === "custom") return 3
|
||||
return 0
|
||||
}
|
||||
|
||||
function loadFields() {
|
||||
selectedCoverPath = ""
|
||||
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 || ""
|
||||
|
||||
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]) })
|
||||
}
|
||||
} else {
|
||||
nameField.text = ""
|
||||
developerField.text = ""
|
||||
executableField.text = ""
|
||||
workingDirField.text = ""
|
||||
|
||||
runnerCombo.currentIndex = 0
|
||||
runnerPathField.text = ""
|
||||
prefixPathField.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -215,6 +291,168 @@ Kirigami.Dialog {
|
|||
text: ""
|
||||
placeholderText: i18n("Optional")
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormCard {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18n("Compatibility")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
QQC2.Label {
|
||||
text: i18n("Runner")
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
QQC2.ComboBox {
|
||||
id: runnerCombo
|
||||
Layout.fillWidth: true
|
||||
model: [i18n("Native"), i18n("Wine"), i18n("Proton"), i18n("Custom")]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: runnerPathField
|
||||
label: i18n("Runner Path")
|
||||
text: ""
|
||||
placeholderText: runnerCombo.currentIndex === 2 ? i18n("Auto-detect Proton") : (runnerCombo.currentIndex === 1 ? i18n("Use system Wine") : i18n("Required for Custom"))
|
||||
enabled: runnerCombo.currentIndex !== 0
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Browse Runner...")
|
||||
icon.name: "document-open"
|
||||
enabled: runnerCombo.currentIndex !== 0
|
||||
onClicked: runnerFileDialog.open()
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: prefixPathField
|
||||
label: i18n("Prefix Path")
|
||||
text: ""
|
||||
placeholderText: i18n("Default (AppDataLocation/prefixes/<gameId>)")
|
||||
enabled: runnerCombo.currentIndex === 1 || runnerCombo.currentIndex === 2
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Browse Prefix...")
|
||||
icon.name: "document-open-folder"
|
||||
enabled: runnerCombo.currentIndex === 1 || runnerCombo.currentIndex === 2
|
||||
onClicked: prefixFolderDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormCard {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18n("Environment Variables")
|
||||
}
|
||||
|
||||
FormCard.FormTextDelegate {
|
||||
text: i18n("These variables are applied only when launching this game from A-La-Karte.")
|
||||
textItem.wrapMode: Text.WordWrap
|
||||
textItem.font: Kirigami.Theme.smallFont
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Add Variable")
|
||||
icon.name: "list-add"
|
||||
onClicked: envModel.append({ key: "", value: "" })
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Preset: MangoHud")
|
||||
description: i18n("Sets MANGOHUD=1")
|
||||
icon.name: "games-hint"
|
||||
onClicked: dialog.setEnvVar("MANGOHUD", "1")
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Preset: GameMode")
|
||||
description: i18n("Sets GAMEMODERUN=1")
|
||||
icon.name: "speedometer"
|
||||
onClicked: dialog.setEnvVar("GAMEMODERUN", "1")
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Clear Variables")
|
||||
icon.name: "edit-clear"
|
||||
enabled: envModel.count > 0
|
||||
onClicked: envModel.clear()
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Repeater {
|
||||
model: envModel
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.TextField {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
placeholderText: i18n("KEY")
|
||||
text: key
|
||||
onTextChanged: envModel.setProperty(index, "key", text)
|
||||
}
|
||||
|
||||
QQC2.TextField {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 2
|
||||
placeholderText: i18n("VALUE")
|
||||
text: value
|
||||
onTextChanged: envModel.setProperty(index, "value", text)
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
icon.name: "list-remove"
|
||||
onClicked: envModel.remove(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormCard {
|
||||
|
|
@ -339,6 +577,23 @@ Kirigami.Dialog {
|
|||
}
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: runnerFileDialog
|
||||
title: i18n("Select Runner Executable")
|
||||
fileMode: FileDialog.OpenFile
|
||||
onAccepted: {
|
||||
runnerPathField.text = selectedFile.toString().replace("file://", "")
|
||||
}
|
||||
}
|
||||
|
||||
FolderDialog {
|
||||
id: prefixFolderDialog
|
||||
title: i18n("Select Prefix Folder")
|
||||
onAccepted: {
|
||||
prefixPathField.text = selectedFolder.toString().replace("file://", "")
|
||||
}
|
||||
}
|
||||
|
||||
function saveCoverImage(game) {
|
||||
if (selectedCoverPath === "") return
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue