a-la-karte/src/qml/SettingsPage.qml

2045 lines
76 KiB
QML

// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.alakarte
import "components"
ColumnLayout {
id: settingsPage
spacing: Kirigami.Units.mediumSpacing
property string runnerInstallUrl: ""
property string runnerInstallSha256: ""
property string runnerInstallName: ""
property string compatibilityGameId: ""
property string compatibilityLoadedRunnerId: ""
property string compatibilityLoadedRunner: ""
property string compatibilityLastError: ""
property int compatibilitySpecRevision: 0
readonly property bool compatibilityHasGame: (compatibilityGameId || "").trim() !== ""
property int compatibilityRunnerIndex: 0
property string compatibilityRunnerPath: ""
property string compatibilityPrefixPath: ""
ListModel {
id: compatibilityGameChoicesModel
}
ListModel {
id: compatibilityRunnerChoicesModel
}
ListModel {
id: compatibilityEnvModel
}
ListModel {
id: compatibilityExtraArgsModel
}
ListModel {
id: compatibilityDllOverridesModel
}
property string lastControllerInput: ""
property string lastControllerInputTime: ""
property string profileMenuControllerId: ""
property string newProfileName: ""
property string pendingDeleteProfileId: ""
property string pendingDeleteProfileName: ""
function focusFirstControl() {
showPlatformBadgesDelegate.forceActiveFocus()
}
property var pendingDisableImportApply: null
property string pendingDisableImportName: ""
readonly property bool anyConfirmOpen: disableImportConfirmDialog.opened
|| fetchAllCoversConfirmDialog.opened
|| removeMissingConfirmDialog.opened
|| clearConfirmDialog.opened
|| resetConfirmDialog.opened
|| deleteProfileConfirmDialog.opened
readonly property bool anyMenuOpen: uiModeMenu.visible || profileMenu.visible
function currentConfirmDialog() {
if (disableImportConfirmDialog.opened) return disableImportConfirmDialog
if (fetchAllCoversConfirmDialog.opened) return fetchAllCoversConfirmDialog
if (removeMissingConfirmDialog.opened) return removeMissingConfirmDialog
if (clearConfirmDialog.opened) return clearConfirmDialog
if (resetConfirmDialog.opened) return resetConfirmDialog
if (deleteProfileConfirmDialog.opened) return deleteProfileConfirmDialog
return null
}
function closeCurrentConfirmDialog() {
let d = currentConfirmDialog()
if (!d) return
if (typeof d.reject === "function") {
d.reject()
} else {
d.close()
}
}
function closeCurrentMenu() {
let m = currentMenu()
if (m && m.close) m.close()
}
function currentMenu() {
if (profileMenu.visible) return profileMenu
if (uiModeMenu.visible) return uiModeMenu
return null
}
function isDescendant(item, ancestor) {
let p = item
while (p) {
if (p === ancestor) return true
if (ancestor.contentItem && p === ancestor.contentItem) return true
if (p.visualParent !== undefined && p.visualParent !== null) {
if (settingsPage.isDescendant(p.visualParent, ancestor)) return true
} else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) {
if (settingsPage.isDescendant(p.popup.visualParent, ancestor)) return true
}
p = p.parent
}
return false
}
function focusNextInMenu(forward) {
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
let menu = currentMenu()
if (!menu) return
let next = w.activeFocusItem
for (let i = 0; i < 50; i++) {
next = next.nextItemInFocusChain(forward)
if (!next) return
if (settingsPage.isDescendant(next, menu)) {
next.forceActiveFocus()
return
}
}
}
function rebuildCompatibilityGameChoices() {
compatibilityGameChoicesModel.clear()
if (!App || !App.gameModel) return
for (let i = 0; i < App.gameModel.count; i++) {
let g = App.gameModel.gameAt(i)
if (!g) continue
compatibilityGameChoicesModel.append({ gameId: String(g.id || ""), label: String(g.name || g.id || "") })
}
if (compatibilityGameId === "") {
if (compatibilityGameChoicesModel.count > 0) {
compatibilityGameId = String(compatibilityGameChoicesModel.get(0).gameId || "")
}
}
}
function rebuildCompatibilityRunnerChoices() {
compatibilityRunnerChoicesModel.clear()
compatibilityRunnerChoicesModel.append({ kind: "native", label: i18n("Native"), runner: "", runnerId: "", runnerType: "" })
compatibilityRunnerChoicesModel.append({ kind: "wine", label: i18n("Wine (system)"), runner: "wine", runnerId: "", runnerType: "wine" })
compatibilityRunnerChoicesModel.append({ kind: "proton", label: i18n("Proton (auto)"), runner: "proton", runnerId: "", runnerType: "proton" })
let runners = (App.runnerManager && App.runnerManager.runners) ? App.runnerManager.runners : []
for (let i = 0; i < runners.length; i++) {
let r = runners[i]
if (!r) continue
let name = r.name ? String(r.name) : ""
let type = r.type ? String(r.type) : ""
let source = r.source ? String(r.source) : ""
let label = name
if (type !== "" || source !== "") {
label = name + " (" + type + (source !== "" ? (" · " + source) : "") + ")"
}
compatibilityRunnerChoicesModel.append({ kind: "inventory", label: label, runner: "", runnerId: String(r.id || ""), runnerType: type })
}
compatibilityRunnerChoicesModel.append({ kind: "custom", label: i18n("Custom runner path"), runner: "custom", runnerId: "", runnerType: "custom" })
}
function compatibilityIndexForGameId(gameId) {
let id = (gameId || "").trim()
if (id === "") return 0
for (let i = 0; i < compatibilityGameChoicesModel.count; i++) {
let row = compatibilityGameChoicesModel.get(i)
if (row && String(row.gameId || "") === id) return i
}
return 0
}
function compatibilityCurrentRunnerChoice() {
if (!compatibilityRunnerChoicesModel || compatibilityRunnerChoicesModel.count <= 0) return null
let idx = settingsPage.compatibilityRunnerIndex
if (idx < 0 || idx >= compatibilityRunnerChoicesModel.count) idx = 0
return compatibilityRunnerChoicesModel.get(idx)
}
function compatibilityCurrentRunnerChoiceKind() {
let c = settingsPage.compatibilityCurrentRunnerChoice()
if (!c || !c.kind) return "native"
return String(c.kind)
}
function compatibilityCurrentRunnerChoiceRunnerType() {
let c = settingsPage.compatibilityCurrentRunnerChoice()
if (!c) return ""
if (c.runnerType) return String(c.runnerType)
if (c.runner) return String(c.runner)
return ""
}
function compatibilityIndexForRunnerId(runnerId) {
let id = (runnerId || "").trim()
if (id === "") return 0
for (let i = 0; i < compatibilityRunnerChoicesModel.count; i++) {
let row = compatibilityRunnerChoicesModel.get(i)
if (row.kind === "inventory" && String(row.runnerId || "") === id) return i
}
return 0
}
function compatibilityIndexForRunner(runner) {
let r = (runner || "").trim().toLowerCase()
if (r === "wine") return 1
if (r === "proton") return 2
if (r === "custom") return Math.max(0, compatibilityRunnerChoicesModel.count - 1)
return 0
}
function compatibilitySyncRunnerIndexFromLoaded() {
if ((settingsPage.compatibilityLoadedRunnerId || "") !== "") {
settingsPage.compatibilityRunnerIndex = settingsPage.compatibilityIndexForRunnerId(settingsPage.compatibilityLoadedRunnerId)
return
}
if ((settingsPage.compatibilityLoadedRunner || "") !== "") {
settingsPage.compatibilityRunnerIndex = settingsPage.compatibilityIndexForRunner(settingsPage.compatibilityLoadedRunner)
return
}
settingsPage.compatibilityRunnerIndex = 0
}
function compatibilitySetEnvFromMap(envMap) {
compatibilityEnvModel.clear()
if (!envMap) return
let keys = Object.keys(envMap)
keys.sort()
for (let i = 0; i < keys.length; i++) {
let k = keys[i]
compatibilityEnvModel.append({ key: String(k), value: String(envMap[k] || "") })
}
}
function compatibilityEnvModelToMap() {
let result = ({})
for (let i = 0; i < compatibilityEnvModel.count; i++) {
let row = compatibilityEnvModel.get(i)
let k = (row.key || "").trim()
if (k === "") continue
if (k.indexOf("=") !== -1) continue
result[k] = String(row.value || "")
}
return result
}
function compatibilitySetExtraArgsFromList(list) {
compatibilityExtraArgsModel.clear()
if (!list || !Array.isArray(list)) return
for (let i = 0; i < list.length; i++) {
let a = String(list[i] || "").trim()
if (a === "") continue
compatibilityExtraArgsModel.append({ arg: a })
}
}
function compatibilityExtraArgsModelToList() {
let result = []
for (let i = 0; i < compatibilityExtraArgsModel.count; i++) {
let row = compatibilityExtraArgsModel.get(i)
let a = (row.arg || "").trim()
if (a === "") continue
result.push(a)
}
return result
}
function compatibilitySetDllOverridesFromMap(dllMap) {
compatibilityDllOverridesModel.clear()
if (!dllMap) return
let keys = Object.keys(dllMap)
keys.sort()
for (let i = 0; i < keys.length; i++) {
let k = String(keys[i] || "").trim()
if (k === "") continue
compatibilityDllOverridesModel.append({ key: k, value: String(dllMap[k] || "") })
}
}
function compatibilityDllOverridesModelToMap() {
let result = ({})
for (let i = 0; i < compatibilityDllOverridesModel.count; i++) {
let row = compatibilityDllOverridesModel.get(i)
let k = (row.key || "").trim()
if (k === "") continue
if (k.indexOf("=") !== -1) continue
if (k.indexOf(";") !== -1) continue
let v = (row.value || "").trim()
if (v === "") continue
result[k] = String(v)
}
return result
}
function compatibilitySetEnvVar(key, value) {
let k = (key || "").trim()
if (k === "") return
for (let i = 0; i < compatibilityEnvModel.count; i++) {
let row = compatibilityEnvModel.get(i)
if ((row.key || "").trim() === k) {
compatibilityEnvModel.setProperty(i, "value", String(value || ""))
settingsPage.compatibilityBumpSpecRevision()
return
}
}
compatibilityEnvModel.append({ key: k, value: String(value || "") })
settingsPage.compatibilityBumpSpecRevision()
}
function compatibilityBumpSpecRevision() {
settingsPage.compatibilitySpecRevision = settingsPage.compatibilitySpecRevision + 1
}
function compatibilityResetEditor() {
settingsPage.compatibilityLastError = ""
settingsPage.compatibilityLoadedRunnerId = ""
settingsPage.compatibilityLoadedRunner = ""
settingsPage.compatibilityRunnerPath = ""
settingsPage.compatibilityPrefixPath = ""
settingsPage.compatibilityRunnerIndex = 0
compatibilityEnvModel.clear()
compatibilityExtraArgsModel.clear()
compatibilityDllOverridesModel.clear()
settingsPage.compatibilityBumpSpecRevision()
}
function compatibilityApplyProfile(profile) {
let p = profile || ({})
settingsPage.compatibilityLastError = ""
settingsPage.compatibilityLoadedRunnerId = String(p.runnerId || "")
settingsPage.compatibilityLoadedRunner = String(p.runner || "")
settingsPage.compatibilityRunnerPath = String(p.runnerPath || "")
settingsPage.compatibilityPrefixPath = String(p.prefixPath || "")
settingsPage.compatibilitySyncRunnerIndexFromLoaded()
settingsPage.compatibilitySetEnvFromMap(p.envOverrides || ({}))
settingsPage.compatibilitySetExtraArgsFromList(p.extraArgs || ([]))
settingsPage.compatibilitySetDllOverridesFromMap(p.dllOverrides || ({}))
settingsPage.compatibilityBumpSpecRevision()
}
function compatibilityRequestProfile() {
let id = (settingsPage.compatibilityGameId || "").trim()
if (id === "") return
if (!App || !App.runnerManager) return
App.runnerManager.requestGameProfile(id)
}
function compatibilityBuildProfileSpec() {
let id = (settingsPage.compatibilityGameId || "").trim()
if (id === "") return ({})
let choice = settingsPage.compatibilityCurrentRunnerChoice()
let kind = choice && choice.kind ? String(choice.kind) : "native"
let runnerType = settingsPage.compatibilityCurrentRunnerChoiceRunnerType()
let runnerId = choice && choice.runnerId ? String(choice.runnerId) : ""
let runner = choice && choice.runner ? String(choice.runner) : ""
let spec = ({ gameId: id })
if (runnerId !== "") spec.runnerId = runnerId
if (runner !== "") spec.runner = runner
if ((kind === "custom" || kind === "wine" || kind === "proton") && (settingsPage.compatibilityRunnerPath || "").trim() !== "") {
spec.runnerPath = settingsPage.compatibilityRunnerPath.trim()
}
if ((runnerType === "wine" || runnerType === "proton") && (settingsPage.compatibilityPrefixPath || "").trim() !== "") {
spec.prefixPath = settingsPage.compatibilityPrefixPath.trim()
}
let env = settingsPage.compatibilityEnvModelToMap()
if (Object.keys(env).length > 0) spec.envOverrides = env
let extraArgs = settingsPage.compatibilityExtraArgsModelToList()
if (extraArgs.length > 0) spec.extraArgs = extraArgs
let dllOverrides = settingsPage.compatibilityDllOverridesModelToMap()
if (Object.keys(dllOverrides).length > 0) spec.dllOverrides = dllOverrides
return spec
}
function compatibilityCanSaveProfile() {
let spec = settingsPage.compatibilityBuildProfileSpec()
if (!spec || Object.keys(spec).length <= 1) return false
return true
}
function compatibilitySaveProfile() {
let id = (settingsPage.compatibilityGameId || "").trim()
if (id === "") return
if (!App || !App.runnerManager) return
let spec = settingsPage.compatibilityBuildProfileSpec()
if (!spec || Object.keys(spec).length <= 1) return
App.runnerManager.setGameProfile(spec)
}
function compatibilityClearProfile() {
let id = (settingsPage.compatibilityGameId || "").trim()
if (id === "") return
if (!App || !App.runnerManager) return
App.runnerManager.clearGameProfile(id)
}
onCompatibilityGameIdChanged: {
settingsPage.compatibilityResetEditor()
settingsPage.compatibilityRequestProfile()
}
Component.onCompleted: {
settingsPage.rebuildCompatibilityGameChoices()
settingsPage.rebuildCompatibilityRunnerChoices()
settingsPage.compatibilityRequestProfile()
}
Connections {
target: App && App.gameModel ? App.gameModel : null
function onCountChanged() {
settingsPage.rebuildCompatibilityGameChoices()
}
}
Connections {
target: App && App.runnerManager ? App.runnerManager : null
function onRunnersChanged() {
settingsPage.rebuildCompatibilityRunnerChoices()
settingsPage.compatibilitySyncRunnerIndexFromLoaded()
}
function onPrefixEnsured(gameId, prefixPath) {
let id = String(gameId || "")
if (id !== settingsPage.compatibilityGameId) return
settingsPage.compatibilityLastError = ""
if ((settingsPage.compatibilityPrefixPath || "").trim() !== "") {
settingsPage.compatibilityPrefixPath = String(prefixPath || "")
}
}
function onPrefixDeleted(gameId, prefixPath) {
let id = String(gameId || "")
if (id !== settingsPage.compatibilityGameId) return
settingsPage.compatibilityLastError = ""
}
function onGameProfileFetched(gameId, result) {
let id = String(gameId || "")
if (id !== settingsPage.compatibilityGameId) return
if (!result || result.ok !== true) {
settingsPage.compatibilityLastError = result && result.error ? String(result.error) : i18n("Failed to load profile")
settingsPage.compatibilityApplyProfile({})
return
}
settingsPage.compatibilityApplyProfile(result.profile || ({}))
}
function onGameProfileSaved(gameId, result) {
let id = String(gameId || "")
if (id !== settingsPage.compatibilityGameId) return
if (!result || result.ok !== true) {
settingsPage.compatibilityLastError = result && result.error ? String(result.error) : i18n("Failed to save profile")
return
}
settingsPage.compatibilityApplyProfile(result.profile || ({}))
}
function onGameProfileCleared(gameId, result) {
let id = String(gameId || "")
if (id !== settingsPage.compatibilityGameId) return
if (!result || result.ok !== true) {
settingsPage.compatibilityLastError = result && result.error ? String(result.error) : i18n("Failed to clear profile")
return
}
settingsPage.compatibilityResetEditor()
}
}
FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true
title: i18n("Controller")
}
FormCard.FormCard {
Layout.fillWidth: true
FormCard.FormTextDelegate {
text: GamepadManager.connected ? i18n("Connected") : i18n("Not connected")
description: GamepadManager.connected ? (GamepadManager.name !== "" ? GamepadManager.name : i18n("Gamepad")) : i18n("Connect a controller to use couch navigation")
textItem.wrapMode: Text.WordWrap
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextDelegate {
text: i18n("Device")
description: {
if (!GamepadManager.connected) return i18n("-")
let parts = []
if (GamepadManager.vendorId > 0) parts.push(i18n("Vendor: %1").arg(GamepadManager.vendorId))
if (GamepadManager.productId > 0) parts.push(i18n("Product: %1").arg(GamepadManager.productId))
return parts.length > 0 ? parts.join(" · ") : i18n("-")
}
textItem.wrapMode: Text.WordWrap
descriptionItem.wrapMode: Text.WordWrap
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextDelegate {
text: i18n("Buttons")
description: {
if (!GamepadManager.connected) return i18n("-")
return i18n("Confirm: %1 · Back: %2 · Details: %3 · Search: %4")
.arg(GamepadManager.confirmButtonLabel)
.arg(GamepadManager.backButtonLabel)
.arg(GamepadManager.detailsButtonLabel)
.arg(GamepadManager.searchButtonLabel)
}
textItem.wrapMode: Text.WordWrap
descriptionItem.wrapMode: Text.WordWrap
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextDelegate {
text: i18n("Active input")
description: {
if (InputManager.activeInput === InputManager.Gamepad) return i18n("Gamepad")
return i18n("Keyboard & Mouse")
}
textItem.wrapMode: Text.WordWrap
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextDelegate {
text: i18n("Input service")
description: {
if (App.inputService.available) return i18n("Available")
if (App.inputService.lastError !== "") return App.inputService.lastError
return i18n("Unavailable")
}
descriptionItem.wrapMode: Text.WordWrap
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Refresh controllers")
icon.name: "view-refresh"
onClicked: App.inputService.refreshControllers()
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Refresh profiles")
icon.name: "view-refresh"
onClicked: App.inputService.refreshProfiles()
}
}
FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true
title: i18n("Controllers")
}
FormCard.FormCard {
Layout.fillWidth: true
Repeater {
model: App.inputService.controllers
ColumnLayout {
Layout.fillWidth: true
readonly property var controller: modelData
readonly property string controllerId: controller && controller.controllerId ? controller.controllerId : ""
readonly property string controllerName: controller && controller.name ? controller.name : controllerId
readonly property string connectionState: controller && controller.connectionState ? controller.connectionState : ""
readonly property int batteryPercent: controller && controller.batteryPercent !== undefined ? controller.batteryPercent : -1
readonly property string batteryState: controller && controller.batteryStateName ? controller.batteryStateName : ""
readonly property string activeProfileName: controller && controller.activeProfileName ? controller.activeProfileName : ""
readonly property string activeProfileId: controller && controller.activeProfileId ? controller.activeProfileId : "default"
FormCard.FormTextDelegate {
text: controllerName
description: {
let parts = []
if (connectionState !== "") parts.push(connectionState)
if (batteryPercent >= 0) {
parts.push(i18n("Battery: %1%2").arg(batteryPercent).arg("%"))
} else if (batteryState !== "") {
parts.push(i18n("Battery: %1").arg(batteryState))
}
return parts.length > 0 ? parts.join(" · ") : ""
}
textItem.wrapMode: Text.WordWrap
descriptionItem.wrapMode: Text.WordWrap
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Profile")
description: activeProfileName !== "" ? activeProfileName : i18n("Default")
icon.name: "preferences-desktop-gaming"
enabled: controllerId !== "" && App.inputService.profiles.length > 0
onClicked: {
settingsPage.profileMenuControllerId = controllerId
profileMenu.open()
}
}
FormCard.FormDelegateSeparator {}
}
}
FormCard.FormTextDelegate {
visible: App.inputService.controllers.length === 0
text: i18n("No controllers detected")
textItem.font: Kirigami.Theme.smallFont
}
}
QQC2.Menu {
id: profileMenu
focus: true
onOpened: Qt.callLater(function() {
if (profileMenu.contentChildren.length > 0 && profileMenu.contentChildren[0].forceActiveFocus) {
profileMenu.contentChildren[0].forceActiveFocus()
}
})
Repeater {
model: App.inputService.profiles
delegate: QQC2.MenuItem {
readonly property var profile: modelData
readonly property string profileId: profile && profile.id ? profile.id : ""
text: profile && profile.name ? profile.name : profileId
checkable: true
checked: profileId !== "" && profileId === settingsPage.currentProfileIdForMenu()
onTriggered: {
if (profileId === "" || settingsPage.profileMenuControllerId === "") return
App.inputService.setActiveProfile(settingsPage.profileMenuControllerId, profileId)
}
}
}
}
function currentProfileIdForMenu() {
for (let i = 0; i < App.inputService.controllers.length; i++) {
let c = App.inputService.controllers[i]
if (c && c.controllerId === settingsPage.profileMenuControllerId) {
return c.activeProfileId ? c.activeProfileId : "default"
}
}
return "default"
}
FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true
title: i18n("Controller Profiles")
}
FormCard.FormCard {
Layout.fillWidth: true
FormCard.FormTextFieldDelegate {
label: i18n("New profile name")
text: settingsPage.newProfileName
placeholderText: i18n("e.g. Couch")
onTextChanged: settingsPage.newProfileName = text
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Create profile")
icon.name: "list-add"
enabled: settingsPage.newProfileName.trim() !== "" && App.inputService.available
onClicked: {
App.inputService.createProfile(settingsPage.newProfileName.trim())
settingsPage.newProfileName = ""
}
}
FormCard.FormDelegateSeparator {}
Repeater {
model: App.inputService.profiles
ColumnLayout {
Layout.fillWidth: true
readonly property var profile: modelData
readonly property string profileId: profile && profile.id ? profile.id : ""
readonly property string profileName: profile && profile.name ? profile.name : profileId
FormCard.FormTextDelegate {
text: profileName
description: profileId
descriptionItem.wrapMode: Text.WrapAnywhere
descriptionItem.font: Kirigami.Theme.smallFont
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Delete")
icon.name: "edit-delete"
enabled: profileId !== "" && profileId !== "default" && App.inputService.available
onClicked: {
settingsPage.pendingDeleteProfileId = profileId
settingsPage.pendingDeleteProfileName = profileName
deleteProfileConfirmDialog.open()
}
}
FormCard.FormDelegateSeparator {}
}
}
}
FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true
title: i18n("Input Test")
}
FormCard.FormCard {
Layout.fillWidth: true
FormCard.FormTextDelegate {
text: settingsPage.lastControllerInput !== "" ? settingsPage.lastControllerInput : i18n("No input yet")
description: settingsPage.lastControllerInputTime
textItem.wrapMode: Text.WordWrap
descriptionItem.wrapMode: Text.WordWrap
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Clear")
icon.name: "edit-clear"
enabled: settingsPage.lastControllerInput !== ""
onClicked: {
settingsPage.lastControllerInput = ""
settingsPage.lastControllerInputTime = ""
}
}
}
function activateFocusedInMenu() {
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
let menu = currentMenu()
if (!menu) return
let item = w.activeFocusItem
if (typeof item.triggered === "function") {
item.triggered()
return
}
if (typeof item.clicked === "function") {
item.clicked()
return
}
}
function focusNoButton(dialog) {
Qt.callLater(function() {
if (!dialog) return
if (typeof dialog.standardButton === "function") {
let noButton = dialog.standardButton(Kirigami.Dialog.No)
if (noButton) {
noButton.forceActiveFocus()
return
}
}
dialog.forceActiveFocus()
})
}
function requestDisableImport(sourceName, applyFn) {
pendingDisableImportName = sourceName
pendingDisableImportApply = applyFn
disableImportConfirmDialog.open()
}
FormCard.FormHeader {
Layout.fillWidth: true
title: i18n("Appearance")
}
FormCard.FormCard {
Layout.fillWidth: true
FormCard.FormCheckDelegate {
id: showPlatformBadgesDelegate
text: i18n("Show platform badges")
description: i18n("Display platform icons on game cards")
checked: App.config.showPlatformBadges
onToggled: App.config.showPlatformBadges = checked
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
id: uiModeDelegate
text: i18n("UI mode")
description: {
if (App.config.uiMode === Config.Desktop) return i18n("Desktop")
if (App.config.uiMode === Config.Couch) return i18n("Couch")
return i18n("Automatic")
}
icon.name: "view-fullscreen"
onClicked: uiModeMenu.open()
QQC2.Menu {
id: uiModeMenu
focus: true
onOpened: Qt.callLater(function() { uiModeAuto.forceActiveFocus() })
QQC2.MenuItem {
id: uiModeAuto
text: i18n("Automatic")
checkable: true
checked: App.config.uiMode === Config.Auto
onTriggered: App.config.uiMode = Config.Auto
}
QQC2.MenuItem {
id: uiModeDesktop
text: i18n("Desktop")
checkable: true
checked: App.config.uiMode === Config.Desktop
onTriggered: App.config.uiMode = Config.Desktop
}
QQC2.MenuItem {
id: uiModeCouch
text: i18n("Couch")
checkable: true
checked: App.config.uiMode === Config.Couch
onTriggered: App.config.uiMode = Config.Couch
}
}
}
}
Connections {
target: GamepadManager
function onNavigateUp() {
if (!settingsPage.anyMenuOpen) return
let menu = settingsPage.currentMenu()
if (!menu) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!settingsPage.isDescendant(w.activeFocusItem, menu)) return
settingsPage.focusNextInMenu(false)
}
function onNavigateDown() {
if (!settingsPage.anyMenuOpen) return
let menu = settingsPage.currentMenu()
if (!menu) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!settingsPage.isDescendant(w.activeFocusItem, menu)) return
settingsPage.focusNextInMenu(true)
}
function onNavigateLeft() {
if (!settingsPage.anyMenuOpen) return
let menu = settingsPage.currentMenu()
if (!menu) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!settingsPage.isDescendant(w.activeFocusItem, menu)) return
settingsPage.focusNextInMenu(false)
}
function onNavigateRight() {
if (!settingsPage.anyMenuOpen) return
let menu = settingsPage.currentMenu()
if (!menu) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!settingsPage.isDescendant(w.activeFocusItem, menu)) return
settingsPage.focusNextInMenu(true)
}
function onSelectPressed() {
if (!settingsPage.anyMenuOpen) return
let menu = settingsPage.currentMenu()
if (!menu) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!settingsPage.isDescendant(w.activeFocusItem, menu)) return
settingsPage.activateFocusedInMenu()
}
function onBackPressed() {
settingsPage.lastControllerInput = i18n("Back")
settingsPage.lastControllerInputTime = new Date().toLocaleTimeString()
}
function onDetailsPressed() {
settingsPage.lastControllerInput = i18n("Details")
settingsPage.lastControllerInputTime = new Date().toLocaleTimeString()
}
function onMenuPressed() {
settingsPage.lastControllerInput = i18n("Menu")
settingsPage.lastControllerInputTime = new Date().toLocaleTimeString()
}
function onSearchPressed() {
settingsPage.lastControllerInput = i18n("Search")
settingsPage.lastControllerInputTime = new Date().toLocaleTimeString()
}
function onLeftBumperPressed() {
settingsPage.lastControllerInput = i18n("Left bumper")
settingsPage.lastControllerInputTime = new Date().toLocaleTimeString()
}
function onRightBumperPressed() {
settingsPage.lastControllerInput = i18n("Right bumper")
settingsPage.lastControllerInputTime = new Date().toLocaleTimeString()
}
}
Kirigami.PromptDialog {
id: deleteProfileConfirmDialog
title: i18n("Delete Profile")
subtitle: settingsPage.pendingDeleteProfileName !== ""
? i18n("Delete '%1'?", settingsPage.pendingDeleteProfileName)
: i18n("Delete this profile?")
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
onOpened: settingsPage.focusNoButton(deleteProfileConfirmDialog)
onAccepted: {
if (settingsPage.pendingDeleteProfileId !== "") {
App.inputService.deleteProfile(settingsPage.pendingDeleteProfileId)
}
settingsPage.pendingDeleteProfileId = ""
settingsPage.pendingDeleteProfileName = ""
}
onRejected: {
settingsPage.pendingDeleteProfileId = ""
settingsPage.pendingDeleteProfileName = ""
}
}
FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true
title: i18n("Import Sources")
}
FormCard.FormCard {
Layout.fillWidth: true
FormCard.FormCheckDelegate {
text: i18n("Steam")
icon.name: ""
leading: IconWithResourceFallback {
primary: "com.valvesoftware.Steam"
secondary: "steam"
resourceFallback: Qt.resolvedUrl("icons/brand/steam-symbolic.svg")
}
property bool restoring: false
checked: App.config.importSteam
onToggled: {
if (restoring) return
if (checked === App.config.importSteam) return
if (!checked && App.gameModel.hasPlatformPrefix("Steam")) {
restoring = true
checked = Qt.binding(function() { return App.config.importSteam })
restoring = false
settingsPage.requestDisableImport(i18n("Steam"), function() { App.config.importSteam = false })
return
}
App.config.importSteam = checked
restoring = true
checked = Qt.binding(function() { return App.config.importSteam })
restoring = false
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormCheckDelegate {
text: i18n("Lutris")
icon.name: ""
leading: IconWithResourceFallback {
primary: "lutris"
secondary: "applications-games"
}
property bool restoring: false
checked: App.config.importLutris
onToggled: {
if (restoring) return
if (checked === App.config.importLutris) return
if (!checked && App.gameModel.hasPlatformPrefix("Lutris")) {
restoring = true
checked = Qt.binding(function() { return App.config.importLutris })
restoring = false
settingsPage.requestDisableImport(i18n("Lutris"), function() { App.config.importLutris = false })
return
}
App.config.importLutris = checked
restoring = true
checked = Qt.binding(function() { return App.config.importLutris })
restoring = false
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormCheckDelegate {
text: i18n("Heroic Games Launcher")
description: i18n("Epic, GOG, and Amazon games")
icon.name: ""
leading: IconWithResourceFallback {
primary: "com.heroicgameslauncher.hgl"
secondary: "applications-games"
}
property bool restoring: false
checked: App.config.importHeroic
onToggled: {
if (restoring) return
if (checked === App.config.importHeroic) return
if (!checked && App.gameModel.hasPlatformPrefix("Heroic")) {
restoring = true
checked = Qt.binding(function() { return App.config.importHeroic })
restoring = false
settingsPage.requestDisableImport(i18n("Heroic Games Launcher"), function() { App.config.importHeroic = false })
return
}
App.config.importHeroic = checked
restoring = true
checked = Qt.binding(function() { return App.config.importHeroic })
restoring = false
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormCheckDelegate {
text: i18n("Desktop Entries")
description: i18n("Games from .desktop files")
icon.name: ""
leading: IconWithResourceFallback {
primary: "user-desktop"
secondary: "computer"
}
property bool restoring: false
checked: App.config.importDesktop
onToggled: {
if (restoring) return
if (checked === App.config.importDesktop) return
if (!checked && App.gameModel.hasPlatformPrefix("Desktop")) {
restoring = true
checked = Qt.binding(function() { return App.config.importDesktop })
restoring = false
settingsPage.requestDisableImport(i18n("Desktop Entries"), function() { App.config.importDesktop = false })
return
}
App.config.importDesktop = checked
restoring = true
checked = Qt.binding(function() { return App.config.importDesktop })
restoring = false
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormCheckDelegate {
text: i18n("Bottles")
description: i18n("Wine applications")
icon.name: ""
leading: IconWithResourceFallback {
primary: "com.usebottles.bottles"
secondary: "application-x-executable"
}
property bool restoring: false
checked: App.config.importBottles
onToggled: {
if (restoring) return
if (checked === App.config.importBottles) return
if (!checked && App.gameModel.hasPlatformPrefix("Bottles")) {
restoring = true
checked = Qt.binding(function() { return App.config.importBottles })
restoring = false
settingsPage.requestDisableImport(i18n("Bottles"), function() { App.config.importBottles = false })
return
}
App.config.importBottles = checked
restoring = true
checked = Qt.binding(function() { return App.config.importBottles })
restoring = false
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormCheckDelegate {
text: i18n("Flatpak")
description: i18n("Flatpak game applications")
icon.name: ""
leading: IconWithResourceFallback {
primary: "flatpak-discover"
secondary: "applications-games"
}
property bool restoring: false
checked: App.config.importFlatpak
onToggled: {
if (restoring) return
if (checked === App.config.importFlatpak) return
if (!checked && App.gameModel.hasPlatformPrefix("Flatpak")) {
restoring = true
checked = Qt.binding(function() { return App.config.importFlatpak })
restoring = false
settingsPage.requestDisableImport(i18n("Flatpak"), function() { App.config.importFlatpak = false })
return
}
App.config.importFlatpak = checked
restoring = true
checked = Qt.binding(function() { return App.config.importFlatpak })
restoring = false
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormCheckDelegate {
text: i18n("itch.io")
description: i18n("Games from itch.io app")
icon.name: ""
leading: IconWithResourceFallback {
primary: "io.itch.itch"
secondary: "itch"
resourceFallback: Qt.resolvedUrl("icons/brand/itchdotio-symbolic.svg")
}
property bool restoring: false
checked: App.config.importItch
onToggled: {
if (restoring) return
if (checked === App.config.importItch) return
if (!checked && App.gameModel.hasPlatformPrefix("itch.io")) {
restoring = true
checked = Qt.binding(function() { return App.config.importItch })
restoring = false
settingsPage.requestDisableImport(i18n("itch.io"), function() { App.config.importItch = false })
return
}
App.config.importItch = checked
restoring = true
checked = Qt.binding(function() { return App.config.importItch })
restoring = false
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormCheckDelegate {
text: i18n("Legendary")
description: i18n("Epic Games via Legendary CLI")
icon.name: ""
leading: IconWithResourceFallback {
primary: "legendary"
secondary: "applications-games"
}
property bool restoring: false
checked: App.config.importLegendary
onToggled: {
if (restoring) return
if (checked === App.config.importLegendary) return
if (!checked && App.gameModel.hasPlatformPrefix("Legendary")) {
restoring = true
checked = Qt.binding(function() { return App.config.importLegendary })
restoring = false
settingsPage.requestDisableImport(i18n("Legendary"), function() { App.config.importLegendary = false })
return
}
App.config.importLegendary = checked
restoring = true
checked = Qt.binding(function() { return App.config.importLegendary })
restoring = false
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormCheckDelegate {
text: i18n("RetroArch")
description: i18n("Games from RetroArch playlists")
icon.name: ""
leading: IconWithResourceFallback {
primary: "org.libretro.RetroArch"
secondary: "retroarch"
resourceFallback: Qt.resolvedUrl("icons/brand/retroarch-symbolic.svg")
}
property bool restoring: false
checked: App.config.importRetroArch
onToggled: {
if (restoring) return
if (checked === App.config.importRetroArch) return
if (!checked && App.gameModel.hasPlatformPrefix("RetroArch")) {
restoring = true
checked = Qt.binding(function() { return App.config.importRetroArch })
restoring = false
settingsPage.requestDisableImport(i18n("RetroArch"), function() { App.config.importRetroArch = false })
return
}
App.config.importRetroArch = checked
restoring = true
checked = Qt.binding(function() { return App.config.importRetroArch })
restoring = false
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormCheckDelegate {
text: i18n("Auto-import on startup")
description: i18n("Scan for new games when launching")
checked: App.config.autoImportOnStartup
onToggled: App.config.autoImportOnStartup = checked
}
}
FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true
title: i18n("Behavior")
}
FormCard.FormCard {
Layout.fillWidth: true
FormCard.FormCheckDelegate {
text: i18n("Exit after launching game")
description: i18n("Close A-La-Karte when a game starts")
checked: App.config.exitAfterLaunch
onToggled: App.config.exitAfterLaunch = checked
}
FormCard.FormDelegateSeparator {}
FormCard.FormCheckDelegate {
text: i18n("Click cover to launch")
description: i18n("Clicking the cover launches the game instead of showing details")
checked: App.config.coverLaunchesGame
onToggled: App.config.coverLaunchesGame = checked
}
}
FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true
title: i18n("Runners")
}
FormCard.FormCard {
Layout.fillWidth: true
FormCard.FormTextFieldDelegate {
label: i18n("Archive URL or local path")
text: settingsPage.runnerInstallUrl
placeholderText: i18n("https://... or /home/.../file.tar.gz")
enabled: !App.runnerManager.busy
onTextChanged: settingsPage.runnerInstallUrl = text
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextFieldDelegate {
label: i18n("SHA256/SHA512 (optional)")
text: settingsPage.runnerInstallSha256
placeholderText: i18n("Leave empty to skip verification")
enabled: !App.runnerManager.busy
onTextChanged: settingsPage.runnerInstallSha256 = text
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextFieldDelegate {
label: i18n("Name (optional)")
text: settingsPage.runnerInstallName
placeholderText: i18n("Proton-GE")
enabled: !App.runnerManager.busy
onTextChanged: settingsPage.runnerInstallName = text
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Install from URL")
description: App.runnerManager.busy ? App.runnerManager.status : i18n("Download and install runner")
icon.name: "download"
enabled: settingsPage.runnerInstallUrl.trim() !== "" && !App.runnerManager.busy
onClicked: App.runnerManager.installRunnerFromUrl(settingsPage.runnerInstallUrl, settingsPage.runnerInstallSha256, settingsPage.runnerInstallName)
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Cancel install")
icon.name: "dialog-cancel"
enabled: App.runnerManager.busy
onClicked: App.runnerManager.cancelCurrentInstall()
}
FormCard.FormDelegateSeparator {}
Item {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
spacing: Kirigami.Units.smallSpacing
QQC2.Label {
Layout.fillWidth: true
text: App.runnerManager.status
wrapMode: Text.WordWrap
visible: App.runnerManager.status !== ""
}
QQC2.Label {
Layout.fillWidth: true
text: App.runnerManager.lastError
wrapMode: Text.WordWrap
visible: App.runnerManager.lastError !== ""
color: Kirigami.Theme.negativeTextColor
}
QQC2.ProgressBar {
Layout.fillWidth: true
from: 0
to: App.runnerManager.totalBytes > 0 ? App.runnerManager.totalBytes : 1
value: App.runnerManager.receivedBytes
indeterminate: App.runnerManager.busy && App.runnerManager.totalBytes <= 0
visible: App.runnerManager.busy
}
}
}
}
FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true
title: i18n("Installed Runners")
}
FormCard.FormCard {
Layout.fillWidth: true
FormCard.FormButtonDelegate {
text: i18n("Refresh")
icon.name: "view-refresh"
enabled: !App.runnerManager.busy
onClicked: App.runnerManager.refreshRunners()
}
FormCard.FormDelegateSeparator {}
Repeater {
model: App.runnerManager.runners
ColumnLayout {
Layout.fillWidth: true
readonly property var runner: modelData
readonly property string runnerId: (runner && runner.id) ? runner.id : ""
readonly property string runnerName: (runner && runner.name) ? runner.name : runnerId
readonly property string runnerType: (runner && runner.type) ? runner.type : ""
readonly property string runnerSource: (runner && runner.source) ? runner.source : ""
readonly property string runnerPath: (runner && runner.path) ? runner.path : ""
FormCard.FormTextDelegate {
text: runnerName
description: (runnerType !== "" ? (runnerType + " · ") : "") + (runnerSource !== "" ? runnerSource : "")
textItem.wrapMode: Text.WordWrap
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextDelegate {
text: runnerPath
textItem.wrapMode: Text.WrapAnywhere
textItem.font: Kirigami.Theme.smallFont
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Uninstall")
icon.name: "edit-delete"
enabled: runnerSource === "installed" && runnerId !== "" && !App.runnerManager.busy
onClicked: App.runnerManager.uninstallRunner(runnerId)
}
FormCard.FormDelegateSeparator {}
}
}
FormCard.FormTextDelegate {
visible: App.runnerManager.runners.length === 0
text: i18n("No runners found")
textItem.font: Kirigami.Theme.smallFont
}
}
FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true
title: i18n("Compatibility")
}
FormCard.FormCard {
Layout.fillWidth: true
FormCard.FormComboBoxDelegate {
text: i18n("Game")
model: compatibilityGameChoicesModel
textRole: "label"
currentIndex: settingsPage.compatibilityIndexForGameId(settingsPage.compatibilityGameId)
onActivated: {
if (currentIndex < 0 || currentIndex >= compatibilityGameChoicesModel.count) return
settingsPage.compatibilityGameId = String(compatibilityGameChoicesModel.get(currentIndex).gameId || "")
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Reload profile")
icon.name: "view-refresh"
enabled: settingsPage.compatibilityHasGame
onClicked: settingsPage.compatibilityRequestProfile()
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextDelegate {
visible: settingsPage.compatibilityLastError !== ""
text: settingsPage.compatibilityLastError
textItem.wrapMode: Text.WordWrap
textItem.color: Kirigami.Theme.negativeTextColor
textItem.font: Kirigami.Theme.smallFont
}
FormCard.FormDelegateSeparator {}
FormCard.FormComboBoxDelegate {
text: i18n("Runner")
model: compatibilityRunnerChoicesModel
textRole: "label"
currentIndex: settingsPage.compatibilityRunnerIndex
enabled: settingsPage.compatibilityHasGame
onActivated: {
settingsPage.compatibilityRunnerIndex = currentIndex
settingsPage.compatibilityBumpSpecRevision()
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextFieldDelegate {
label: i18n("Runner Path")
text: settingsPage.compatibilityRunnerPath
placeholderText: settingsPage.compatibilityCurrentRunnerChoiceKind() === "proton" ? i18n("Auto-detect Proton") : (settingsPage.compatibilityCurrentRunnerChoiceKind() === "wine" ? i18n("Use system Wine") : i18n("Required for Custom"))
enabled: settingsPage.compatibilityHasGame && (settingsPage.compatibilityCurrentRunnerChoiceKind() === "custom" || settingsPage.compatibilityCurrentRunnerChoiceKind() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceKind() === "proton")
onTextChanged: {
settingsPage.compatibilityRunnerPath = text
settingsPage.compatibilityBumpSpecRevision()
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextFieldDelegate {
label: i18n("Prefix Path")
text: settingsPage.compatibilityPrefixPath
placeholderText: i18n("Default (AppDataLocation/prefixes/<gameId>)")
enabled: settingsPage.compatibilityHasGame && (settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "proton")
onTextChanged: {
settingsPage.compatibilityPrefixPath = text
settingsPage.compatibilityBumpSpecRevision()
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Create Prefix")
icon.name: "folder-new"
enabled: settingsPage.compatibilityHasGame
&& (settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "proton")
onClicked: App.runnerManager.ensurePrefix(settingsPage.compatibilityGameId, settingsPage.compatibilityCurrentRunnerChoiceRunnerType(), settingsPage.compatibilityPrefixPath.trim())
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Delete Prefix")
icon.name: "edit-delete"
enabled: settingsPage.compatibilityHasGame
&& (settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "proton")
&& (settingsPage.compatibilityPrefixPath.trim() === "")
onClicked: App.runnerManager.deletePrefix(settingsPage.compatibilityGameId, settingsPage.compatibilityPrefixPath.trim())
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Reset Prefix to Default")
icon.name: "edit-clear"
enabled: settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "proton"
onClicked: settingsPage.compatibilityPrefixPath = ""
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Save Profile")
icon.name: "document-save"
enabled: settingsPage.compatibilityHasGame && settingsPage.compatibilitySpecRevision >= 0 && settingsPage.compatibilityCanSaveProfile()
onClicked: settingsPage.compatibilitySaveProfile()
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Clear Profile")
icon.name: "edit-delete"
enabled: settingsPage.compatibilityHasGame
onClicked: settingsPage.compatibilityClearProfile()
}
}
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"
enabled: settingsPage.compatibilityHasGame
onClicked: {
compatibilityEnvModel.append({ key: "", value: "" })
settingsPage.compatibilityBumpSpecRevision()
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Preset: MangoHud")
description: i18n("Sets MANGOHUD=1")
icon.name: "games-hint"
enabled: settingsPage.compatibilityHasGame
onClicked: settingsPage.compatibilitySetEnvVar("MANGOHUD", "1")
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Preset: GameMode")
description: i18n("Sets GAMEMODERUN=1")
icon.name: "speedometer"
enabled: settingsPage.compatibilityHasGame
onClicked: settingsPage.compatibilitySetEnvVar("GAMEMODERUN", "1")
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Clear Variables")
icon.name: "edit-clear"
enabled: settingsPage.compatibilityHasGame && compatibilityEnvModel.count > 0
onClicked: {
compatibilityEnvModel.clear()
settingsPage.compatibilityBumpSpecRevision()
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.smallSpacing
Repeater {
model: compatibilityEnvModel
RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
QQC2.TextField {
Layout.fillWidth: true
Layout.preferredWidth: 1
placeholderText: i18n("KEY")
text: key
enabled: settingsPage.compatibilityHasGame
onTextChanged: {
compatibilityEnvModel.setProperty(index, "key", text)
settingsPage.compatibilityBumpSpecRevision()
}
}
QQC2.TextField {
Layout.fillWidth: true
Layout.preferredWidth: 2
placeholderText: i18n("VALUE")
text: value
enabled: settingsPage.compatibilityHasGame
onTextChanged: {
compatibilityEnvModel.setProperty(index, "value", text)
settingsPage.compatibilityBumpSpecRevision()
}
}
QQC2.ToolButton {
icon.name: "list-remove"
enabled: settingsPage.compatibilityHasGame
onClicked: {
compatibilityEnvModel.remove(index)
settingsPage.compatibilityBumpSpecRevision()
}
}
}
}
}
}
FormCard.FormCard {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.largeSpacing
FormCard.FormHeader {
title: i18n("Extra Launch Arguments")
}
FormCard.FormTextDelegate {
text: i18n("These arguments are appended when launching this game from A-La-Karte.")
textItem.wrapMode: Text.WordWrap
textItem.font: Kirigami.Theme.smallFont
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Add Argument")
icon.name: "list-add"
enabled: settingsPage.compatibilityHasGame
onClicked: {
compatibilityExtraArgsModel.append({ arg: "" })
settingsPage.compatibilityBumpSpecRevision()
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Clear Arguments")
icon.name: "edit-clear"
enabled: settingsPage.compatibilityHasGame && compatibilityExtraArgsModel.count > 0
onClicked: {
compatibilityExtraArgsModel.clear()
settingsPage.compatibilityBumpSpecRevision()
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.smallSpacing
Repeater {
model: compatibilityExtraArgsModel
RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
QQC2.TextField {
Layout.fillWidth: true
placeholderText: i18n("ARG")
text: arg
enabled: settingsPage.compatibilityHasGame
onTextChanged: {
compatibilityExtraArgsModel.setProperty(index, "arg", text)
settingsPage.compatibilityBumpSpecRevision()
}
}
QQC2.ToolButton {
icon.name: "list-remove"
enabled: settingsPage.compatibilityHasGame
onClicked: {
compatibilityExtraArgsModel.remove(index)
settingsPage.compatibilityBumpSpecRevision()
}
}
}
}
}
}
FormCard.FormCard {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.largeSpacing
FormCard.FormHeader {
title: i18n("Wine DLL Overrides")
}
FormCard.FormTextDelegate {
text: i18n("These overrides are applied to Wine/Proton launches via WINEDLLOVERRIDES.")
textItem.wrapMode: Text.WordWrap
textItem.font: Kirigami.Theme.smallFont
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Add Override")
icon.name: "list-add"
enabled: settingsPage.compatibilityHasGame && (settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "proton")
onClicked: {
compatibilityDllOverridesModel.append({ key: "", value: "" })
settingsPage.compatibilityBumpSpecRevision()
}
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Clear Overrides")
icon.name: "edit-clear"
enabled: settingsPage.compatibilityHasGame && compatibilityDllOverridesModel.count > 0
onClicked: {
compatibilityDllOverridesModel.clear()
settingsPage.compatibilityBumpSpecRevision()
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.smallSpacing
Repeater {
model: compatibilityDllOverridesModel
RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
QQC2.TextField {
Layout.fillWidth: true
Layout.preferredWidth: 1
placeholderText: i18n("DLL")
text: key
enabled: settingsPage.compatibilityHasGame && (settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "proton")
onTextChanged: {
compatibilityDllOverridesModel.setProperty(index, "key", text)
settingsPage.compatibilityBumpSpecRevision()
}
}
QQC2.TextField {
Layout.fillWidth: true
Layout.preferredWidth: 2
placeholderText: i18n("OVERRIDE")
text: value
enabled: settingsPage.compatibilityHasGame && (settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "proton")
onTextChanged: {
compatibilityDllOverridesModel.setProperty(index, "value", text)
settingsPage.compatibilityBumpSpecRevision()
}
}
QQC2.ToolButton {
icon.name: "list-remove"
enabled: settingsPage.compatibilityHasGame && (settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "wine" || settingsPage.compatibilityCurrentRunnerChoiceRunnerType() === "proton")
onClicked: {
compatibilityDllOverridesModel.remove(index)
settingsPage.compatibilityBumpSpecRevision()
}
}
}
}
}
}
FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true
title: i18n("Cover Art")
}
FormCard.FormCard {
Layout.fillWidth: true
FormCard.FormCheckDelegate {
text: i18n("High quality images")
description: i18n("Sharper covers at the cost of memory")
checked: App.config.highQualityImages
onToggled: App.config.highQualityImages = checked
}
FormCard.FormDelegateSeparator {}
FormCard.FormCheckDelegate {
text: i18n("Animated covers")
description: i18n("Play animated cover art when available")
checked: App.config.animatedCovers
onToggled: App.config.animatedCovers = checked
}
}
FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true
title: i18n("SteamGridDB")
}
FormCard.FormCard {
Layout.fillWidth: true
FormCard.FormCheckDelegate {
text: i18n("Enable SteamGridDB")
description: i18n("Automatically download cover art")
checked: App.steamGridDB.enabled
onToggled: App.steamGridDB.enabled = checked
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextFieldDelegate {
label: i18n("SteamGridDB API Key")
text: App.steamGridDB.apiKey
placeholderText: i18n("Enter your API key")
echoMode: TextInput.Password
enabled: App.steamGridDB.enabled
onTextChanged: App.steamGridDB.apiKey = text
}
FormCard.FormDelegateSeparator {}
FormCard.FormCheckDelegate {
text: i18n("Prefer SteamGridDB covers")
description: i18n("Replace existing covers with SteamGridDB art")
checked: App.steamGridDB.preferSteamGridDB
enabled: App.steamGridDB.enabled
onToggled: App.steamGridDB.preferSteamGridDB = checked
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Fetch All Covers")
description: App.steamGridDB.busy ? i18n("Fetching...") : i18n("Download covers for all games")
icon.name: "download"
enabled: App.steamGridDB.enabled && App.steamGridDB.apiKey.length > 0 && !App.steamGridDB.busy
onClicked: fetchAllCoversConfirmDialog.open()
}
FormCard.FormDelegateSeparator {}
FormCard.FormTextDelegate {
text: i18n("Get a free API key at steamgriddb.com")
textItem.font: Kirigami.Theme.smallFont
textItem.color: Kirigami.Theme.linkColor
TapHandler {
onTapped: Qt.openUrlExternally("https://www.steamgriddb.com/profile/preferences/api")
}
}
}
FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true
title: i18n("Library")
}
FormCard.FormCard {
Layout.fillWidth: true
FormCard.FormButtonDelegate {
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")))
onClicked: App.importAllGames()
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Remove Missing Games")
description: i18n("Remove games whose executables no longer exist")
icon.name: "edit-delete"
onClicked: removeMissingConfirmDialog.open()
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Clear Library")
description: i18n("Remove all games")
icon.name: "edit-clear-all"
onClicked: clearConfirmDialog.open()
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Reset Application")
description: i18n("Clear library and reset all settings to defaults")
icon.name: "edit-reset"
onClicked: resetConfirmDialog.open()
}
}
Kirigami.PromptDialog {
id: disableImportConfirmDialog
title: i18n("Disable Import Source")
subtitle: i18n("Disabling %1 will remove all games imported from that source. Are you sure?", settingsPage.pendingDisableImportName)
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
onOpened: settingsPage.focusNoButton(disableImportConfirmDialog)
onAccepted: {
if (settingsPage.pendingDisableImportApply) {
settingsPage.pendingDisableImportApply()
}
settingsPage.pendingDisableImportApply = null
settingsPage.pendingDisableImportName = ""
}
onRejected: {
settingsPage.pendingDisableImportApply = null
settingsPage.pendingDisableImportName = ""
}
}
Kirigami.PromptDialog {
id: fetchAllCoversConfirmDialog
title: i18n("Fetch All Covers")
subtitle: App.steamGridDB.preferSteamGridDB
? i18n("This will download cover art for all games and may replace existing covers. Continue?")
: i18n("This will download cover art for games that are missing covers. Continue?")
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
onOpened: settingsPage.focusNoButton(fetchAllCoversConfirmDialog)
onAccepted: App.steamGridDB.fetchAllCovers()
}
Kirigami.PromptDialog {
id: removeMissingConfirmDialog
title: i18n("Remove Missing Games")
subtitle: i18n("This will remove games whose executables cannot be found. This cannot be undone. Continue?")
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
onOpened: settingsPage.focusNoButton(removeMissingConfirmDialog)
onAccepted: App.removeMissingGames()
}
Kirigami.PromptDialog {
id: clearConfirmDialog
title: i18n("Clear Library")
subtitle: i18n("Are you sure you want to remove all games?")
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
onOpened: settingsPage.focusNoButton(clearConfirmDialog)
onAccepted: App.clearLibrary()
}
Kirigami.PromptDialog {
id: resetConfirmDialog
title: i18n("Reset Application")
subtitle: i18n("This will remove all games and reset all settings to defaults. Are you sure?")
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
onOpened: settingsPage.focusNoButton(resetConfirmDialog)
onAccepted: {
App.clearLibrary()
App.config.resetToDefaults()
}
}
}