mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-02-09 13:03:09 +00:00
Initial release of A-La-Karte, a unified game launcher for KDE Plasma. Includes the QML UI, platform importers, AppStream metadata, icons, and developer documentation.
258 lines
9.3 KiB
QML
258 lines
9.3 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
|
|
|
|
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 = ""
|
|
}
|
|
}
|