mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-03-26 17:03:08 +00:00
862 lines
32 KiB
QML
862 lines
32 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 QtQuick.Dialogs
|
|
import org.kde.kirigami as Kirigami
|
|
import org.kde.kirigamiaddons.formcard as FormCard
|
|
import org.kde.alakarte
|
|
|
|
Kirigami.Dialog {
|
|
id: dialog
|
|
|
|
property var game: null
|
|
property bool isEditing: game !== null
|
|
|
|
ListModel {
|
|
id: envModel
|
|
}
|
|
|
|
ListModel {
|
|
id: runnerChoicesModel
|
|
}
|
|
|
|
signal gameCreated(var game)
|
|
signal gameUpdated(var game)
|
|
|
|
title: isEditing ? i18n("Edit Game") : i18n("Add New Game")
|
|
standardButtons: Kirigami.Dialog.NoButton
|
|
|
|
width: Math.min(parent.width - Kirigami.Units.gridUnit * 4, Kirigami.Units.gridUnit * 30)
|
|
height: Math.min(parent.height - Kirigami.Units.gridUnit * 4, implicitHeight)
|
|
|
|
x: Math.round((parent.width - width) / 2)
|
|
y: Math.round((parent.height - height) / 2)
|
|
|
|
customFooterActions: [
|
|
Kirigami.Action {
|
|
text: isEditing ? i18n("Apply") : i18n("Add")
|
|
icon.name: isEditing ? "dialog-ok-apply" : "list-add"
|
|
enabled: nameField.text.trim() !== "" && executableField.text.trim() !== "" && (dialog.currentRunnerChoiceKind() !== "custom" || runnerPathField.text.trim() !== "")
|
|
onTriggered: {
|
|
let choice = dialog.currentRunnerChoice()
|
|
let runnerKind = choice && choice.kind ? String(choice.kind) : "native"
|
|
let runnerType = choice && choice.runnerType ? String(choice.runnerType) : (choice && choice.runner ? String(choice.runner) : "")
|
|
|
|
let runnerValue = choice && choice.runner ? String(choice.runner) : ""
|
|
let runnerIdValue = choice && choice.runnerId ? String(choice.runnerId) : ""
|
|
|
|
let runnerPathValue = runnerPathField.text.trim()
|
|
let prefixPathValue = prefixPathField.text.trim()
|
|
|
|
if (runnerKind === "native") {
|
|
runnerValue = ""
|
|
runnerIdValue = ""
|
|
runnerPathValue = ""
|
|
prefixPathValue = ""
|
|
} else if (runnerKind === "inventory") {
|
|
runnerValue = ""
|
|
runnerPathValue = ""
|
|
} else if (runnerKind === "wine" || runnerKind === "proton") {
|
|
runnerIdValue = ""
|
|
} else if (runnerKind === "custom") {
|
|
runnerIdValue = ""
|
|
prefixPathValue = ""
|
|
}
|
|
|
|
if (runnerType !== "wine" && runnerType !== "proton") {
|
|
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.launchRunnerId = runnerIdValue
|
|
game.launchRunnerPath = runnerPathValue
|
|
game.launchPrefixPath = prefixPathValue
|
|
if (selectedCoverPath !== "") {
|
|
App.setCoverFromFile(game, selectedCoverPath)
|
|
}
|
|
App.saveLibrary()
|
|
gameUpdated(game)
|
|
} else {
|
|
let newGame = App.createGame(nameField.text.trim(), executableField.text.trim())
|
|
if (newGame) {
|
|
newGame.developer = developerField.text.trim()
|
|
newGame.workingDirectory = workingDirField.text.trim()
|
|
newGame.launchEnv = dialog.envModelToMap()
|
|
newGame.launchRunner = runnerValue
|
|
newGame.launchRunnerId = runnerIdValue
|
|
newGame.launchRunnerPath = runnerPathValue
|
|
newGame.launchPrefixPath = prefixPathValue
|
|
if (selectedCoverPath !== "") {
|
|
App.setCoverFromFile(newGame, selectedCoverPath)
|
|
}
|
|
App.saveLibrary()
|
|
gameCreated(newGame)
|
|
}
|
|
}
|
|
dialog.close()
|
|
}
|
|
},
|
|
Kirigami.Action {
|
|
text: i18n("Cancel")
|
|
icon.name: "dialog-cancel"
|
|
onTriggered: dialog.close()
|
|
}
|
|
]
|
|
|
|
property string selectedCoverPath: ""
|
|
|
|
readonly property bool anyConfirmOpen: !!(deletePrefixConfirmDialog && deletePrefixConfirmDialog.opened)
|
|
|
|
readonly property bool anyMenuOpen: {
|
|
if (!runnerCombo || !runnerCombo.popup) return false
|
|
return runnerCombo.popup.visible === true
|
|
}
|
|
|
|
function currentConfirmDialog() {
|
|
if (deletePrefixConfirmDialog && deletePrefixConfirmDialog.opened) return deletePrefixConfirmDialog
|
|
return null
|
|
}
|
|
|
|
function closeCurrentConfirmDialog() {
|
|
let d = currentConfirmDialog()
|
|
if (!d) return
|
|
if (typeof d.reject === "function") {
|
|
d.reject()
|
|
} else {
|
|
d.close()
|
|
}
|
|
}
|
|
|
|
function closeCurrentMenu() {
|
|
if (runnerCombo && runnerCombo.popup && runnerCombo.popup.visible === true) {
|
|
runnerCombo.popup.close()
|
|
}
|
|
}
|
|
|
|
function isDescendant(item, ancestor) {
|
|
let p = item
|
|
while (p) {
|
|
if (p === ancestor || (ancestor.contentItem && p === ancestor.contentItem)) return true
|
|
if (p.visualParent !== undefined && p.visualParent !== null) {
|
|
if (dialog.isDescendant(p.visualParent, ancestor)) return true
|
|
} else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) {
|
|
if (dialog.isDescendant(p.popup.visualParent, ancestor)) return true
|
|
}
|
|
p = p.parent
|
|
}
|
|
return false
|
|
}
|
|
|
|
function focusNextInChain(forward) {
|
|
let w = applicationWindow()
|
|
if (!w || !w.activeFocusItem) return
|
|
let next = w.activeFocusItem
|
|
for (let i = 0; i < 50; i++) {
|
|
next = next.nextItemInFocusChain(forward)
|
|
if (!next) return
|
|
if (dialog.isDescendant(next, dialog)) {
|
|
next.forceActiveFocus()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
function activateFocused() {
|
|
let w = applicationWindow()
|
|
if (!w || !w.activeFocusItem) return
|
|
let item = w.activeFocusItem
|
|
|
|
if (typeof item.toggle === "function") {
|
|
item.toggle()
|
|
return
|
|
}
|
|
if (item.checkable !== undefined && item.checkable && item.checked !== undefined) {
|
|
item.checked = !item.checked
|
|
return
|
|
}
|
|
if (typeof item.clicked === "function") {
|
|
item.clicked()
|
|
return
|
|
}
|
|
}
|
|
|
|
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 currentRunnerChoice() {
|
|
if (!runnerChoicesModel || runnerChoicesModel.count <= 0) return null
|
|
let idx = runnerCombo ? runnerCombo.currentIndex : 0
|
|
if (idx < 0 || idx >= runnerChoicesModel.count) return runnerChoicesModel.get(0)
|
|
return runnerChoicesModel.get(idx)
|
|
}
|
|
|
|
function currentRunnerChoiceKind() {
|
|
let c = dialog.currentRunnerChoice()
|
|
if (!c || !c.kind) return "native"
|
|
return String(c.kind)
|
|
}
|
|
|
|
function currentRunnerChoiceRunnerType() {
|
|
let c = dialog.currentRunnerChoice()
|
|
if (!c) return ""
|
|
if (c.runnerType) return String(c.runnerType)
|
|
if (c.runner) return String(c.runner)
|
|
return ""
|
|
}
|
|
|
|
function rebuildRunnerChoices() {
|
|
runnerChoicesModel.clear()
|
|
|
|
runnerChoicesModel.append({ kind: "native", label: i18n("Native"), runner: "", runnerId: "", runnerType: "" })
|
|
runnerChoicesModel.append({ kind: "wine", label: i18n("Wine (system)"), runner: "wine", runnerId: "", runnerType: "wine" })
|
|
runnerChoicesModel.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 || !r.id) continue
|
|
let name = r.name ? String(r.name) : String(r.id)
|
|
let source = r.source ? String(r.source) : ""
|
|
let type = r.type ? String(r.type) : ""
|
|
let label = name
|
|
if (type !== "" || source !== "") {
|
|
label = name + " (" + type + (source !== "" ? (" · " + source) : "") + ")"
|
|
}
|
|
runnerChoicesModel.append({ kind: "inventory", label: label, runner: "", runnerId: String(r.id), runnerType: type })
|
|
}
|
|
|
|
runnerChoicesModel.append({ kind: "custom", label: i18n("Custom runner path"), runner: "custom", runnerId: "", runnerType: "custom" })
|
|
}
|
|
|
|
function indexForRunnerId(runnerId) {
|
|
let id = (runnerId || "").trim()
|
|
if (id === "") return 0
|
|
for (let i = 0; i < runnerChoicesModel.count; i++) {
|
|
let row = runnerChoicesModel.get(i)
|
|
if (row.kind === "inventory" && String(row.runnerId || "") === id) return i
|
|
}
|
|
return 0
|
|
}
|
|
|
|
function indexForRunner(runner) {
|
|
let r = (runner || "").trim().toLowerCase()
|
|
if (r === "wine") return 1
|
|
if (r === "proton") return 2
|
|
if (r === "custom") return Math.max(0, runnerChoicesModel.count - 1)
|
|
return 0
|
|
}
|
|
|
|
function loadFields() {
|
|
selectedCoverPath = ""
|
|
envModel.clear()
|
|
dialog.rebuildRunnerChoices()
|
|
if (isEditing && game) {
|
|
nameField.text = game.name || ""
|
|
developerField.text = game.developer || ""
|
|
executableField.text = game.launchCommand || ""
|
|
workingDirField.text = game.workingDirectory || ""
|
|
|
|
let idx = dialog.indexForRunnerId(game.launchRunnerId)
|
|
if (idx === 0) {
|
|
idx = dialog.indexForRunner(game.launchRunner)
|
|
}
|
|
runnerCombo.currentIndex = idx
|
|
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 = ""
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: App.runnerManager
|
|
function onRunnersChanged() {
|
|
if (!dialog.opened) return
|
|
|
|
let desiredRunnerId = ""
|
|
if (isEditing && game) {
|
|
desiredRunnerId = String(game.launchRunnerId || "").trim()
|
|
}
|
|
|
|
let current = dialog.currentRunnerChoice()
|
|
let currentRunnerId = current && current.runnerId ? String(current.runnerId) : ""
|
|
let currentRunner = current && current.runner ? String(current.runner) : ""
|
|
|
|
dialog.rebuildRunnerChoices()
|
|
|
|
if (desiredRunnerId !== "") {
|
|
runnerCombo.currentIndex = dialog.indexForRunnerId(desiredRunnerId)
|
|
} else if (currentRunnerId !== "") {
|
|
runnerCombo.currentIndex = dialog.indexForRunnerId(currentRunnerId)
|
|
} else {
|
|
runnerCombo.currentIndex = dialog.indexForRunner(currentRunner)
|
|
}
|
|
}
|
|
|
|
function onPrefixEnsured(gameId, prefixPath) {
|
|
if (!dialog.opened) return
|
|
if (!isEditing || !game) return
|
|
if (String(gameId) !== String(game.id)) return
|
|
prefixPathField.text = String(prefixPath || "")
|
|
}
|
|
|
|
function onPrefixDeleted(gameId, prefixPath) {
|
|
if (!dialog.opened) return
|
|
if (!isEditing || !game) return
|
|
if (String(gameId) !== String(game.id)) return
|
|
prefixPathField.text = ""
|
|
}
|
|
}
|
|
|
|
function effectivePrefixPath() {
|
|
let p = prefixPathField.text.trim()
|
|
if (p !== "") return p
|
|
if (!isEditing || !game) return ""
|
|
return StandardPaths.writableLocation(StandardPaths.AppDataLocation) + "/prefixes/" + game.id
|
|
}
|
|
|
|
function toFileUrl(path) {
|
|
let p = String(path || "").trim()
|
|
if (p === "") return ""
|
|
if (p.startsWith("~/")) {
|
|
p = StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + p.substring(2)
|
|
}
|
|
if (p.startsWith("file:")) return p
|
|
if (p.startsWith("/")) {
|
|
return "file:///" + encodeURI(p.substring(1))
|
|
}
|
|
return "file:///" + encodeURI(p)
|
|
}
|
|
|
|
Connections {
|
|
target: GamepadManager
|
|
function onNavigateUp() {
|
|
let app = applicationWindow()
|
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
|
if (!dialog.opened) return
|
|
let w = applicationWindow()
|
|
if (!w || !w.activeFocusItem) return
|
|
if (!dialog.isDescendant(w.activeFocusItem, dialog)) return
|
|
dialog.focusNextInChain(false)
|
|
}
|
|
function onNavigateDown() {
|
|
let app = applicationWindow()
|
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
|
if (!dialog.opened) return
|
|
let w = applicationWindow()
|
|
if (!w || !w.activeFocusItem) return
|
|
if (!dialog.isDescendant(w.activeFocusItem, dialog)) return
|
|
dialog.focusNextInChain(true)
|
|
}
|
|
function onNavigateLeft() {
|
|
let app = applicationWindow()
|
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
|
if (!dialog.opened) return
|
|
let w = applicationWindow()
|
|
if (!w || !w.activeFocusItem) return
|
|
if (!dialog.isDescendant(w.activeFocusItem, dialog)) return
|
|
dialog.focusNextInChain(false)
|
|
}
|
|
function onNavigateRight() {
|
|
let app = applicationWindow()
|
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
|
if (!dialog.opened) return
|
|
let w = applicationWindow()
|
|
if (!w || !w.activeFocusItem) return
|
|
if (!dialog.isDescendant(w.activeFocusItem, dialog)) return
|
|
dialog.focusNextInChain(true)
|
|
}
|
|
function onSelectPressed() {
|
|
let app = applicationWindow()
|
|
if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return
|
|
if (!dialog.opened) return
|
|
let w = applicationWindow()
|
|
if (!w || !w.activeFocusItem) return
|
|
if (!dialog.isDescendant(w.activeFocusItem, dialog)) return
|
|
dialog.activateFocused()
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
spacing: 0
|
|
|
|
FormCard.FormCard {
|
|
Layout.fillWidth: true
|
|
|
|
FormCard.FormTextFieldDelegate {
|
|
id: nameField
|
|
label: i18n("Name")
|
|
text: ""
|
|
placeholderText: i18n("Game title")
|
|
onAccepted: developerField.forceActiveFocus()
|
|
}
|
|
|
|
FormCard.FormDelegateSeparator {}
|
|
|
|
FormCard.FormTextFieldDelegate {
|
|
id: developerField
|
|
label: i18n("Developer")
|
|
text: ""
|
|
placeholderText: i18n("Optional")
|
|
onAccepted: executableField.forceActiveFocus()
|
|
}
|
|
|
|
FormCard.FormDelegateSeparator {}
|
|
|
|
FormCard.FormTextFieldDelegate {
|
|
id: executableField
|
|
label: i18n("Executable")
|
|
text: ""
|
|
placeholderText: i18n("/path/to/game or command")
|
|
onAccepted: if (nameField.text.trim() !== "" && text.trim() !== "") {
|
|
dialog.customFooterActions[0].trigger()
|
|
}
|
|
}
|
|
|
|
FormCard.FormDelegateSeparator {}
|
|
|
|
FormCard.FormButtonDelegate {
|
|
text: i18n("Browse...")
|
|
icon.name: "document-open"
|
|
onClicked: fileDialog.open()
|
|
}
|
|
|
|
FormCard.FormDelegateSeparator {}
|
|
|
|
FormCard.FormTextFieldDelegate {
|
|
id: workingDirField
|
|
label: i18n("Working Directory")
|
|
text: ""
|
|
placeholderText: i18n("Optional")
|
|
}
|
|
|
|
FormCard.FormDelegateSeparator {}
|
|
|
|
FormCard.FormCard {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
|
|
|
FormCard.FormHeader {
|
|
title: i18n("Compatibility")
|
|
}
|
|
|
|
FormCard.FormComboBoxDelegate {
|
|
id: runnerCombo
|
|
text: i18n("Runner")
|
|
model: runnerChoicesModel
|
|
textRole: "label"
|
|
}
|
|
|
|
FormCard.FormDelegateSeparator {}
|
|
|
|
FormCard.FormTextFieldDelegate {
|
|
id: runnerPathField
|
|
label: i18n("Runner Path")
|
|
text: ""
|
|
placeholderText: dialog.currentRunnerChoiceKind() === "proton" ? i18n("Auto-detect Proton") : (dialog.currentRunnerChoiceKind() === "wine" ? i18n("Use system Wine") : i18n("Required for Custom"))
|
|
enabled: dialog.currentRunnerChoiceKind() === "custom" || dialog.currentRunnerChoiceKind() === "wine" || dialog.currentRunnerChoiceKind() === "proton"
|
|
}
|
|
|
|
FormCard.FormDelegateSeparator {}
|
|
|
|
FormCard.FormButtonDelegate {
|
|
text: i18n("Browse Runner...")
|
|
icon.name: "document-open"
|
|
enabled: dialog.currentRunnerChoiceKind() === "custom" || dialog.currentRunnerChoiceKind() === "wine" || dialog.currentRunnerChoiceKind() === "proton"
|
|
onClicked: runnerFileDialog.open()
|
|
}
|
|
|
|
FormCard.FormDelegateSeparator {}
|
|
|
|
FormCard.FormTextFieldDelegate {
|
|
id: prefixPathField
|
|
label: i18n("Prefix Path")
|
|
text: ""
|
|
placeholderText: i18n("Default (AppDataLocation/prefixes/<gameId>)")
|
|
enabled: dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton"
|
|
}
|
|
|
|
FormCard.FormDelegateSeparator {}
|
|
|
|
FormCard.FormButtonDelegate {
|
|
text: i18n("Browse Prefix...")
|
|
icon.name: "document-open-folder"
|
|
enabled: dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton"
|
|
onClicked: prefixFolderDialog.open()
|
|
}
|
|
|
|
FormCard.FormDelegateSeparator {}
|
|
|
|
FormCard.FormButtonDelegate {
|
|
text: i18n("Open Prefix Folder")
|
|
icon.name: "folder-open"
|
|
enabled: isEditing && (dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton")
|
|
onClicked: {
|
|
let p = dialog.effectivePrefixPath()
|
|
if (p === "") return
|
|
Qt.openUrlExternally(dialog.toFileUrl(p))
|
|
}
|
|
}
|
|
|
|
FormCard.FormDelegateSeparator {}
|
|
|
|
FormCard.FormButtonDelegate {
|
|
text: i18n("Delete Prefix")
|
|
icon.name: "edit-delete"
|
|
enabled: isEditing
|
|
&& (dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton")
|
|
&& (prefixPathField.text.trim() === "")
|
|
onClicked: deletePrefixConfirmDialog.open()
|
|
}
|
|
|
|
FormCard.FormDelegateSeparator {}
|
|
|
|
FormCard.FormButtonDelegate {
|
|
text: i18n("Create Prefix")
|
|
icon.name: "folder-new"
|
|
enabled: isEditing
|
|
&& (dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton")
|
|
onClicked: App.runnerManager.ensurePrefix(game.id, dialog.currentRunnerChoiceRunnerType(), prefixPathField.text.trim())
|
|
}
|
|
|
|
FormCard.FormDelegateSeparator {}
|
|
|
|
FormCard.FormButtonDelegate {
|
|
text: i18n("Reset Prefix to Default")
|
|
icon.name: "edit-clear"
|
|
enabled: dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton"
|
|
onClicked: prefixPathField.text = ""
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
Layout.margins: Kirigami.Units.largeSpacing
|
|
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 {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
|
|
|
FormCard.FormHeader {
|
|
title: i18n("Cover Art")
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: Kirigami.Units.gridUnit * 8
|
|
Layout.margins: Kirigami.Units.largeSpacing
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
spacing: Kirigami.Units.largeSpacing
|
|
|
|
Rectangle {
|
|
Layout.preferredWidth: Kirigami.Units.gridUnit * 5
|
|
Layout.preferredHeight: Kirigami.Units.gridUnit * 7
|
|
radius: Kirigami.Units.smallSpacing
|
|
color: Kirigami.Theme.alternateBackgroundColor
|
|
border.color: Kirigami.Theme.disabledTextColor
|
|
border.width: 1
|
|
|
|
Image {
|
|
anchors.fill: parent
|
|
anchors.margins: 2
|
|
source: selectedCoverPath !== "" ? "file://" + selectedCoverPath :
|
|
(isEditing && game && game.coverUrl.toString() !== "" ? game.coverUrl : "")
|
|
fillMode: Image.PreserveAspectCrop
|
|
|
|
Kirigami.Icon {
|
|
anchors.centerIn: parent
|
|
source: "image-x-generic"
|
|
width: Kirigami.Units.iconSizes.large
|
|
height: width
|
|
visible: parent.status !== Image.Ready
|
|
opacity: 0.5
|
|
}
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
QQC2.Button {
|
|
text: i18n("Choose Cover...")
|
|
icon.name: "document-open"
|
|
onClicked: coverDialog.open()
|
|
}
|
|
|
|
QQC2.Button {
|
|
text: i18n("Fetch from SteamGridDB")
|
|
icon.name: "download"
|
|
enabled: App.steamGridDB.enabled && App.steamGridDB.apiKey.length > 0 &&
|
|
nameField.text.trim() !== "" && !App.steamGridDB.busy
|
|
visible: App.steamGridDB.enabled
|
|
onClicked: {
|
|
if (isEditing && game) {
|
|
App.steamGridDB.fetchCover(game)
|
|
}
|
|
}
|
|
}
|
|
|
|
QQC2.Button {
|
|
text: i18n("Clear Cover")
|
|
icon.name: "edit-clear"
|
|
visible: selectedCoverPath !== "" || (isEditing && game && game.coverUrl.toString() !== "")
|
|
onClicked: {
|
|
selectedCoverPath = ""
|
|
if (isEditing && game) {
|
|
game.coverUrl = ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FormCard.FormCard {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
|
|
|
FormCard.FormHeader {
|
|
title: i18n("Help")
|
|
}
|
|
|
|
FormCard.FormTextDelegate {
|
|
text: i18n("To launch an executable, enter its full path:\n\"/path/to/game\"\n\nTo open a file with the default application:\nxdg-open \"/path/to/file\"\n\nIf the path contains spaces, wrap it in quotes.")
|
|
textItem.wrapMode: Text.WordWrap
|
|
textItem.font: Kirigami.Theme.smallFont
|
|
}
|
|
}
|
|
}
|
|
|
|
FileDialog {
|
|
id: fileDialog
|
|
title: i18n("Select Executable")
|
|
fileMode: FileDialog.OpenFile
|
|
onAccepted: {
|
|
let path = selectedFile.toString().replace("file://", "")
|
|
if (path.includes(" ")) {
|
|
executableField.text = "\"" + path + "\""
|
|
} else {
|
|
executableField.text = path
|
|
}
|
|
}
|
|
}
|
|
|
|
FileDialog {
|
|
id: coverDialog
|
|
title: i18n("Select Cover Image")
|
|
fileMode: FileDialog.OpenFile
|
|
nameFilters: [i18n("Image files (*.png *.jpg *.jpeg *.webp)"), i18n("All files (*)")]
|
|
onAccepted: {
|
|
selectedCoverPath = selectedFile.toString().replace("file://", "")
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
// Copy cover to app data directory
|
|
let coversPath = StandardPaths.writableLocation(StandardPaths.AppDataLocation) + "/covers"
|
|
let fileName = game.id + ".jpg"
|
|
let destPath = coversPath + "/" + fileName
|
|
|
|
// Use App to copy the file and set cover URL
|
|
App.setCoverFromFile(game, selectedCoverPath)
|
|
}
|
|
|
|
onOpened: {
|
|
App.runnerManager.refreshRunners()
|
|
loadFields()
|
|
nameField.forceActiveFocus()
|
|
}
|
|
|
|
onGameChanged: loadFields()
|
|
|
|
Kirigami.PromptDialog {
|
|
id: deletePrefixConfirmDialog
|
|
title: i18n("Delete Prefix")
|
|
subtitle: (isEditing && game)
|
|
? i18n("Delete the prefix for '%1'? This will remove the entire prefix directory.", game.name)
|
|
: i18n("Delete this prefix? This will remove the entire prefix directory.")
|
|
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
|
|
onAccepted: {
|
|
if (!isEditing || !game) return
|
|
App.runnerManager.deletePrefix(game.id, prefixPathField.text.trim())
|
|
}
|
|
onOpened: {
|
|
Qt.callLater(function() {
|
|
if (typeof deletePrefixConfirmDialog.standardButton === "function") {
|
|
let noButton = deletePrefixConfirmDialog.standardButton(Kirigami.Dialog.No)
|
|
if (noButton) {
|
|
noButton.forceActiveFocus()
|
|
return
|
|
}
|
|
}
|
|
deletePrefixConfirmDialog.forceActiveFocus()
|
|
})
|
|
}
|
|
}
|
|
}
|