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
2026-01-25 13:19:57 +00:00
ListModel {
id: envModel
}
2026-02-09 13:03:38 +00:00
ListModel {
id: runnerChoicesModel
}
2026-01-18 12:13:07 +00:00
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"
2026-02-09 13:03:38 +00:00
enabled: nameField . text . trim ( ) !== "" && executableField . text . trim ( ) !== "" && ( dialog . currentRunnerChoiceKind ( ) !== "custom" || runnerPathField . text . trim ( ) !== "" )
2026-01-18 12:13:07 +00:00
onTriggered: {
2026-02-09 13:03:38 +00:00
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 ) : ""
2026-01-25 13:19:57 +00:00
let runnerPathValue = runnerPathField . text . trim ( )
let prefixPathValue = prefixPathField . text . trim ( )
2026-02-09 13:03:38 +00:00
if ( runnerKind === "native" ) {
runnerValue = ""
runnerIdValue = ""
runnerPathValue = ""
prefixPathValue = ""
} else if ( runnerKind === "inventory" ) {
runnerValue = ""
2026-01-25 13:19:57 +00:00
runnerPathValue = ""
2026-02-09 13:03:38 +00:00
} else if ( runnerKind === "wine" || runnerKind === "proton" ) {
runnerIdValue = ""
} else if ( runnerKind === "custom" ) {
runnerIdValue = ""
prefixPathValue = ""
}
if ( runnerType !== "wine" && runnerType !== "proton" ) {
2026-01-25 13:19:57 +00:00
prefixPathValue = ""
}
2026-01-18 12:13:07 +00:00
if ( isEditing ) {
game . name = nameField . text . trim ( )
game . developer = developerField . text . trim ( )
game . launchCommand = executableField . text . trim ( )
game . workingDirectory = workingDirField . text . trim ( )
2026-01-25 13:19:57 +00:00
game . launchEnv = dialog . envModelToMap ( )
game . launchRunner = runnerValue
2026-02-09 13:03:38 +00:00
game . launchRunnerId = runnerIdValue
2026-01-25 13:19:57 +00:00
game . launchRunnerPath = runnerPathValue
game . launchPrefixPath = prefixPathValue
2026-01-18 12:13:07 +00:00
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 ( )
2026-01-25 13:19:57 +00:00
newGame . launchEnv = dialog . envModelToMap ( )
newGame . launchRunner = runnerValue
2026-02-09 13:03:38 +00:00
newGame . launchRunnerId = runnerIdValue
2026-01-25 13:19:57 +00:00
newGame . launchRunnerPath = runnerPathValue
newGame . launchPrefixPath = prefixPathValue
2026-01-18 12:13:07 +00:00
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-02-09 13:03:38 +00:00
readonly property bool anyConfirmOpen: ! ! ( deletePrefixConfirmDialog && deletePrefixConfirmDialog . opened )
2026-02-13 11:52:28 +00:00
readonly property bool anyMenuOpen: {
if ( ! runnerCombo || ! runnerCombo . popup ) return false
return runnerCombo . popup . visible === true
}
2026-02-09 13:03:38 +00:00
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 ( )
}
}
2026-01-29 18:49:45 +00:00
function closeCurrentMenu ( ) {
2026-02-13 11:52:28 +00:00
if ( runnerCombo && runnerCombo . popup && runnerCombo . popup . visible === true ) {
2026-01-29 18:49:45 +00:00
runnerCombo . popup . close ( )
}
}
2026-01-24 12:27:30 +00:00
function isDescendant ( item , ancestor ) {
let p = item
while ( p ) {
if ( p === ancestor || ( ancestor . contentItem && p === ancestor . contentItem ) ) return true
2026-01-29 18:49:45 +00:00
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
}
2026-01-24 12:27:30 +00:00
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
}
2026-01-29 18:49:45 +00:00
if ( item . checkable !== undefined && item . checkable && item . checked !== undefined ) {
2026-01-24 12:27:30 +00:00
item . checked = ! item . checked
return
}
if ( typeof item . clicked === "function" ) {
item . clicked ( )
return
}
}
2026-01-25 13:19:57 +00:00
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
}
2026-02-09 13:03:38 +00:00
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
}
2026-01-20 09:59:29 +00:00
function loadFields ( ) {
selectedCoverPath = ""
2026-01-25 13:19:57 +00:00
envModel . clear ( )
2026-02-09 13:03:38 +00:00
dialog . rebuildRunnerChoices ( )
2026-01-20 09:59:29 +00:00
if ( isEditing && game ) {
nameField . text = game . name || ""
developerField . text = game . developer || ""
executableField . text = game . launchCommand || ""
workingDirField . text = game . workingDirectory || ""
2026-01-25 13:19:57 +00:00
2026-02-09 13:03:38 +00:00
let idx = dialog . indexForRunnerId ( game . launchRunnerId )
if ( idx === 0 ) {
idx = dialog . indexForRunner ( game . launchRunner )
}
runnerCombo . currentIndex = idx
2026-01-25 13:19:57 +00:00
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 ] ) } )
}
2026-01-20 09:59:29 +00:00
} else {
nameField . text = ""
developerField . text = ""
executableField . text = ""
workingDirField . text = ""
2026-01-25 13:19:57 +00:00
runnerCombo . currentIndex = 0
runnerPathField . text = ""
prefixPathField . text = ""
2026-01-20 09:59:29 +00:00
}
}
2026-02-09 13:03:38 +00:00
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 )
}
2026-01-24 12:27:30 +00:00
Connections {
target: GamepadManager
function onNavigateUp ( ) {
2026-01-29 18:49:45 +00:00
let app = applicationWindow ( )
if ( app && app . currentConfirmDialog && app . currentConfirmDialog ( ) ) return
2026-01-24 12:27:30 +00:00
if ( ! dialog . opened ) return
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! dialog . isDescendant ( w . activeFocusItem , dialog ) ) return
dialog . focusNextInChain ( false )
}
function onNavigateDown ( ) {
2026-01-29 18:49:45 +00:00
let app = applicationWindow ( )
if ( app && app . currentConfirmDialog && app . currentConfirmDialog ( ) ) return
2026-01-24 12:27:30 +00:00
if ( ! dialog . opened ) return
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! dialog . isDescendant ( w . activeFocusItem , dialog ) ) return
dialog . focusNextInChain ( true )
}
function onNavigateLeft ( ) {
2026-01-29 18:49:45 +00:00
let app = applicationWindow ( )
if ( app && app . currentConfirmDialog && app . currentConfirmDialog ( ) ) return
2026-01-24 12:27:30 +00:00
if ( ! dialog . opened ) return
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! dialog . isDescendant ( w . activeFocusItem , dialog ) ) return
dialog . focusNextInChain ( false )
}
function onNavigateRight ( ) {
2026-01-29 18:49:45 +00:00
let app = applicationWindow ( )
if ( app && app . currentConfirmDialog && app . currentConfirmDialog ( ) ) return
2026-01-24 12:27:30 +00:00
if ( ! dialog . opened ) return
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! dialog . isDescendant ( w . activeFocusItem , dialog ) ) return
dialog . focusNextInChain ( true )
}
function onSelectPressed ( ) {
2026-01-29 18:49:45 +00:00
let app = applicationWindow ( )
if ( app && app . currentConfirmDialog && app . currentConfirmDialog ( ) ) return
2026-01-24 12:27:30 +00:00
if ( ! dialog . opened ) return
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! dialog . isDescendant ( w . activeFocusItem , dialog ) ) return
dialog . activateFocused ( )
}
}
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" )
}
2026-01-25 13:19:57 +00:00
FormCard . FormDelegateSeparator { }
FormCard . FormCard {
Layout.fillWidth: true
Layout.topMargin: Kirigami . Units . largeSpacing
FormCard . FormHeader {
title: i18n ( "Compatibility" )
}
2026-02-09 13:03:38 +00:00
FormCard . FormComboBoxDelegate {
id: runnerCombo
text: i18n ( "Runner" )
model: runnerChoicesModel
textRole: "label"
2026-01-25 13:19:57 +00:00
}
FormCard . FormDelegateSeparator { }
FormCard . FormTextFieldDelegate {
id: runnerPathField
label: i18n ( "Runner Path" )
text: ""
2026-02-09 13:03:38 +00:00
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"
2026-01-25 13:19:57 +00:00
}
FormCard . FormDelegateSeparator { }
FormCard . FormButtonDelegate {
text: i18n ( "Browse Runner..." )
icon.name: "document-open"
2026-02-09 13:03:38 +00:00
enabled: dialog . currentRunnerChoiceKind ( ) === "custom" || dialog . currentRunnerChoiceKind ( ) === "wine" || dialog . currentRunnerChoiceKind ( ) === "proton"
2026-01-25 13:19:57 +00:00
onClicked: runnerFileDialog . open ( )
}
FormCard . FormDelegateSeparator { }
FormCard . FormTextFieldDelegate {
id: prefixPathField
label: i18n ( "Prefix Path" )
text: ""
placeholderText: i18n ( "Default (AppDataLocation/prefixes/<gameId>)" )
2026-02-09 13:03:38 +00:00
enabled: dialog . currentRunnerChoiceRunnerType ( ) === "wine" || dialog . currentRunnerChoiceRunnerType ( ) === "proton"
2026-01-25 13:19:57 +00:00
}
FormCard . FormDelegateSeparator { }
FormCard . FormButtonDelegate {
text: i18n ( "Browse Prefix..." )
icon.name: "document-open-folder"
2026-02-09 13:03:38 +00:00
enabled: dialog . currentRunnerChoiceRunnerType ( ) === "wine" || dialog . currentRunnerChoiceRunnerType ( ) === "proton"
2026-01-25 13:19:57 +00:00
onClicked: prefixFolderDialog . open ( )
}
2026-02-09 13:03:38 +00:00
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 = ""
}
2026-01-25 13:19:57 +00:00
}
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 ( )
}
2026-02-09 13:03:38 +00:00
ColumnLayout {
2026-01-25 13:19:57 +00:00
Layout.fillWidth: true
Layout.margins: Kirigami . Units . largeSpacing
2026-02-09 13:03:38 +00:00
spacing: Kirigami . Units . smallSpacing
2026-01-25 13:19:57 +00:00
2026-02-09 13:03:38 +00:00
Repeater {
model: envModel
2026-01-25 13:19:57 +00:00
2026-02-09 13:03:38 +00:00
RowLayout {
Layout.fillWidth: true
spacing: Kirigami . Units . smallSpacing
2026-01-25 13:19:57 +00:00
2026-02-09 13:03:38 +00:00
QQC2 . TextField {
2026-01-25 13:19:57 +00:00
Layout.fillWidth: true
2026-02-09 13:03:38 +00:00
Layout.preferredWidth: 1
placeholderText: i18n ( "KEY" )
text: key
onTextChanged: envModel . setProperty ( index , "key" , text )
}
2026-01-25 13:19:57 +00:00
2026-02-09 13:03:38 +00:00
QQC2 . TextField {
Layout.fillWidth: true
Layout.preferredWidth: 2
placeholderText: i18n ( "VALUE" )
text: value
onTextChanged: envModel . setProperty ( index , "value" , text )
}
2026-01-25 13:19:57 +00:00
2026-02-09 13:03:38 +00:00
QQC2 . ToolButton {
icon.name: "list-remove"
onClicked: envModel . remove ( index )
2026-01-25 13:19:57 +00:00
}
}
}
}
}
2026-01-18 12:13:07 +00:00
}
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://" , "" )
}
}
2026-01-25 13:19:57 +00:00
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://" , "" )
}
}
2026-01-18 12:13:07 +00:00
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-02-09 13:03:38 +00:00
App . runnerManager . refreshRunners ( )
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-02-09 13:03:38 +00:00
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 ( )
} )
}
}
2026-01-18 12:13:07 +00:00
}