2026-01-18 12:13:07 +00:00
// 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 )
2026-01-20 09:59:29 +00:00
height: Math . min ( parent . height - Kirigami . Units . gridUnit * 4 , implicitHeight )
x: Math . round ( ( parent . width - width ) / 2 )
y: Math . round ( ( parent . height - height ) / 2 )
2026-01-18 12:13:07 +00:00
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: ""
2026-01-20 09:59:29 +00:00
function loadFields ( ) {
selectedCoverPath = ""
if ( isEditing && game ) {
nameField . text = game . name || ""
developerField . text = game . developer || ""
executableField . text = game . launchCommand || ""
workingDirField . text = game . workingDirectory || ""
} else {
nameField . text = ""
developerField . text = ""
executableField . text = ""
workingDirField . text = ""
}
}
2026-01-18 12:13:07 +00:00
ColumnLayout {
spacing: 0
FormCard . FormCard {
Layout.fillWidth: true
FormCard . FormTextFieldDelegate {
id: nameField
label: i18n ( "Name" )
2026-01-20 09:59:29 +00:00
text: ""
2026-01-18 12:13:07 +00:00
placeholderText: i18n ( "Game title" )
onAccepted: developerField . forceActiveFocus ( )
}
FormCard . FormDelegateSeparator { }
FormCard . FormTextFieldDelegate {
id: developerField
label: i18n ( "Developer" )
2026-01-20 09:59:29 +00:00
text: ""
2026-01-18 12:13:07 +00:00
placeholderText: i18n ( "Optional" )
onAccepted: executableField . forceActiveFocus ( )
}
FormCard . FormDelegateSeparator { }
FormCard . FormTextFieldDelegate {
id: executableField
label: i18n ( "Executable" )
2026-01-20 09:59:29 +00:00
text: ""
2026-01-18 12:13:07 +00:00
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" )
2026-01-20 09:59:29 +00:00
text: ""
2026-01-18 12:13:07 +00:00
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: {
2026-01-20 09:59:29 +00:00
loadFields ( )
2026-01-18 12:13:07 +00:00
nameField . forceActiveFocus ( )
}
2026-01-20 09:59:29 +00:00
onGameChanged: loadFields ( )
2026-01-18 12:13:07 +00:00
}