diff --git a/src/game.cpp b/src/game.cpp index 32993c3..22b7362 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -185,6 +185,129 @@ 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 +{ + 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 profileId = m_activeLaunchProfile.trimmed(); + if (profileId.isEmpty() || profileId == QLatin1String("default")) { + return result; + } + + const QVariantMap profileConfig = launchProfileConfig(profileId); + 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 +446,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 +469,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 +568,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 +580,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 +656,10 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent) game->setLaunchPrefixPath(prefixPath); } + if (!activeProfile.isEmpty()) { + game->setActiveLaunchProfile(activeProfile); + } + return game; } diff --git a/src/game.h b/src/game.h index 05304b8..7d02266 100644 --- a/src/game.h +++ b/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,13 @@ 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 effectiveLaunchConfig() const; + QString platform() const; void setPlatform(const QString &platform); @@ -129,6 +137,7 @@ Q_SIGNALS: void launchRunnerChanged(); void launchRunnerPathChanged(); void launchPrefixPathChanged(); + void activeLaunchProfileChanged(); void platformChanged(); void platformIdChanged(); void dateAddedChanged(); @@ -153,6 +162,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; diff --git a/src/gamelauncher.cpp b/src/gamelauncher.cpp index eeb9c5b..af90fe9 100644 --- a/src/gamelauncher.cpp +++ b/src/gamelauncher.cpp @@ -153,10 +153,11 @@ 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 QVariantMap effectiveLaunchConfig = game->effectiveLaunchConfig(); + 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 +298,11 @@ 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 QVariantMap effectiveLaunchConfig = game->effectiveLaunchConfig(); + 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 diff --git a/src/qml/GameDetailsSheet.qml b/src/qml/GameDetailsSheet.qml index 9f0c68d..4bb593f 100644 --- a/src/qml/GameDetailsSheet.qml +++ b/src/qml/GameDetailsSheet.qml @@ -418,6 +418,17 @@ Kirigami.OverlaySheet { onClicked: detailsSheet.editRequested() } + QQC2.Button { + icon.name: "preferences-other" + text: game && game.activeLaunchProfile === "couch" ? i18n("Profile: Couch") : i18n("Profile: Default") + display: detailsSheet.useCompactLayout ? QQC2.AbstractButton.TextUnderIcon : QQC2.AbstractButton.TextBesideIcon + enabled: !!game + onClicked: if (game) { + game.activeLaunchProfile = (game.activeLaunchProfile === "couch") ? "default" : "couch" + App.saveLibrary() + } + } + QQC2.Button { icon.name: "dialog-information" text: i18n("Diagnostics") diff --git a/src/qml/GameEditDialog.qml b/src/qml/GameEditDialog.qml index 4729aa4..65188a6 100644 --- a/src/qml/GameEditDialog.qml +++ b/src/qml/GameEditDialog.qml @@ -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,72 @@ 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 = game.activeLaunchProfile === "couch" ? "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 +375,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