// 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 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) customFooterActions: [ Kirigami.Action { text: isEditing ? i18n("Apply") : i18n("Add") icon.name: isEditing ? "dialog-ok-apply" : "list-add" enabled: nameField.text.trim() !== "" && executableField.text.trim() !== "" onTriggered: { if (isEditing) { game.name = nameField.text.trim() game.developer = developerField.text.trim() game.launchCommand = executableField.text.trim() game.workingDirectory = workingDirField.text.trim() 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() 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: "" ColumnLayout { spacing: 0 FormCard.FormCard { Layout.fillWidth: true FormCard.FormTextFieldDelegate { id: nameField label: i18n("Name") text: isEditing && game ? game.name : "" placeholderText: i18n("Game title") onAccepted: developerField.forceActiveFocus() } FormCard.FormDelegateSeparator {} FormCard.FormTextFieldDelegate { id: developerField label: i18n("Developer") text: isEditing && game ? (game.developer || "") : "" placeholderText: i18n("Optional") onAccepted: executableField.forceActiveFocus() } FormCard.FormDelegateSeparator {} FormCard.FormTextFieldDelegate { id: executableField label: i18n("Executable") text: isEditing && game ? game.launchCommand : "" 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: isEditing && game ? (game.workingDirectory || "") : "" placeholderText: i18n("Optional") } } 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://", "") } } 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: { nameField.forceActiveFocus() selectedCoverPath = "" } }