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 org . kde . kirigami as Kirigami
import org . kde . kirigamiaddons . formcard as FormCard
import org . kde . alakarte
import "components"
Kirigami . ApplicationWindow {
id: root
title: i18n ( "A-La-Karte" )
minimumWidth: Kirigami . Units . gridUnit * 25
minimumHeight: Kirigami . Units . gridUnit * 20
width: Kirigami . Units . gridUnit * 55
height: Kirigami . Units . gridUnit * 40
property var selectedGame: null
property string currentSource: "all"
property bool searchActive: false
2026-01-24 19:50:20 +00:00
property bool settingsLayerOpen: false
2026-01-18 12:13:07 +00:00
2026-01-24 12:27:30 +00:00
readonly property string hintContext: {
if ( gameEditDialog . visible ) return "edit"
if ( detailsSheet . opened ) return "details"
if ( importSheet . opened ) return "import"
2026-01-24 19:50:20 +00:00
if ( settingsLayerOpen ) return "settings"
2026-01-24 12:27:30 +00:00
if ( aboutSheet . opened ) return "about"
if ( sidebar . modal && sidebar . opened ) return "sidebar"
return "library"
}
2026-01-18 12:13:07 +00:00
function closeTopmost ( ) {
if ( gameEditDialog . visible ) {
gameEditDialog . close ( )
return true
}
if ( detailsSheet . opened ) {
detailsSheet . close ( )
return true
}
if ( importSheet . opened ) {
importSheet . close ( )
return true
}
2026-01-19 23:15:28 +00:00
if ( aboutSheet . opened ) {
aboutSheet . close ( )
return true
}
2026-01-18 12:13:07 +00:00
if ( sidebar . modal && sidebar . opened ) {
sidebar . close ( )
return true
}
if ( root . pageStack . layers . depth > 1 ) {
root . pageStack . layers . pop ( )
Qt . callLater ( function ( ) { libraryView . restoreFocus ( ) } )
return true
}
if ( searchActive ) {
searchActive = false
2026-01-24 12:27:30 +00:00
libraryView . clearSearch ( )
Qt . callLater ( function ( ) { libraryView . restoreFocus ( ) } )
2026-01-18 12:13:07 +00:00
return true
}
return false
}
function canOpenSettings ( ) {
return ! gameEditDialog . visible
&& ! detailsSheet . opened
&& ! importSheet . opened
2026-01-19 23:15:28 +00:00
&& ! aboutSheet . opened
2026-01-24 19:50:20 +00:00
&& ! ( sidebar . modal && sidebar . opened )
2026-01-18 12:13:07 +00:00
&& root . pageStack . layers . depth <= 1
}
2026-01-24 19:50:20 +00:00
function openSettings ( ) {
if ( settingsLayerOpen ) return
if ( ! root . canOpenSettings ( ) ) return
settingsLayerOpen = true
root . pageStack . layers . push ( settingsLayerComponent )
}
function closeSettings ( ) {
if ( ! settingsLayerOpen ) return
root . pageStack . layers . pop ( )
settingsLayerOpen = false
}
2026-01-24 12:27:30 +00:00
function uiModeLabel ( mode ) {
if ( mode === Config . Desktop ) return i18n ( "Desktop" )
if ( mode === Config . Handheld ) return i18n ( "Handheld" )
return i18n ( "Automatic" )
}
function cycleUiMode ( ) {
if ( App . config . uiMode === Config . Auto ) {
App . config . uiMode = Config . Desktop
} else if ( App . config . uiMode === Config . Desktop ) {
App . config . uiMode = Config . Handheld
} else {
App . config . uiMode = Config . Auto
}
root . showPassiveNotification ( i18n ( "UI mode: %1" , uiModeLabel ( App . config . uiMode ) ) )
}
readonly property bool deviceIsMobile: Kirigami . Settings . isMobile
readonly property bool deviceIsTablet: Kirigami . Settings . tabletMode && ! Kirigami . Settings . isMobile
readonly property bool deviceIsTouchDevice: Kirigami . Settings . tabletMode || Kirigami . Settings . isMobile
readonly property bool isMobile: App . config . uiMode === Config . Handheld ? true : deviceIsMobile
readonly property bool isTablet: App . config . uiMode === Config . Handheld ? true : deviceIsTablet
readonly property bool isTouchDevice: App . config . uiMode === Config . Handheld ? true : ( App . config . uiMode === Config . Desktop ? false : deviceIsTouchDevice )
2026-01-18 12:13:07 +00:00
readonly property bool isNarrowScreen: width < Kirigami . Units . gridUnit * 30
readonly property int adaptiveCardSize: App . config . gridSize
pageStack.globalToolBar.style: Kirigami . ApplicationHeaderStyle . ToolBar
pageStack.columnView.columnResizeMode: Kirigami . ColumnView . SingleColumn
2026-01-24 12:27:30 +00:00
footer: QQC2 . ToolBar {
visible: true
leftPadding: 0
rightPadding: 0
topPadding: Kirigami . Units . smallSpacing
bottomPadding: Kirigami . Units . smallSpacing
contentItem: Item {
implicitHeight: Math . max ( centerHints . implicitHeight , rightExtras . implicitHeight )
readonly property bool compactFooter: ( centerHints . implicitWidth + rightExtras . implicitWidth + Kirigami . Units . gridUnit * 2 ) > width
Kirigami . ShadowedRectangle {
id: centerHints
anchors.horizontalCenter: parent . horizontalCenter
anchors.verticalCenter: parent . verticalCenter
radius: Kirigami . Units . largeSpacing
color: Qt . rgba ( Kirigami . Theme . backgroundColor . r , Kirigami . Theme . backgroundColor . g , Kirigami . Theme . backgroundColor . b , 0.85 )
shadow.size: Kirigami . Units . smallSpacing
shadow.color: Qt . rgba ( 0 , 0 , 0 , 0.25 )
implicitWidth: hintRow . implicitWidth + Kirigami . Units . largeSpacing * 2
implicitHeight: hintRow . implicitHeight + Kirigami . Units . smallSpacing * 2
RowLayout {
id: hintRow
anchors.centerIn: parent
spacing: Kirigami . Units . largeSpacing
BottomHintBar {
context: root . hintContext
Layout.alignment: Qt . AlignVCenter
}
}
}
RowLayout {
id: rightExtras
anchors.right: parent . right
anchors.rightMargin: Kirigami . Units . mediumSpacing
anchors.verticalCenter: parent . verticalCenter
spacing: Kirigami . Units . mediumSpacing
visible: ! parent . compactFooter
QQC2 . Label {
text: i18np ( "%1 game" , "%1 games" , libraryView . gameCount )
color: Kirigami . Theme . disabledTextColor
font.pointSize: Kirigami . Theme . smallFont . pointSize
Layout.alignment: Qt . AlignVCenter
visible: root . hintContext === "library" && libraryView . gameCount > 0
}
QQC2 . Slider {
id: sizeSlider
from: 120
to: 280
stepSize: 20
value: App . config . gridSize
Layout.preferredWidth: Kirigami . Units . gridUnit * 8
Layout.alignment: Qt . AlignVCenter
visible: root . hintContext === "library" && libraryView . gameCount > 0
onMoved: App . config . gridSize = value
QQC2 . ToolTip {
parent: sizeSlider . handle
visible: sizeSlider . pressed
text: Math . round ( sizeSlider . value ) + " px"
}
}
}
}
}
2026-01-18 12:13:07 +00:00
Shortcut {
sequence: "Ctrl+F"
onActivated: {
searchActive = true
Qt . callLater ( function ( ) { libraryView . focusSearch ( ) } )
}
}
Shortcut {
sequence: "Ctrl+I"
onActivated: importSheet . open ( )
}
Shortcut {
sequence: "Ctrl+N"
onActivated: {
gameEditDialog . game = null
gameEditDialog . open ( )
}
}
Shortcut {
sequence: StandardKey . Preferences
2026-01-24 19:50:20 +00:00
onActivated: root . openSettings ( )
2026-01-18 12:13:07 +00:00
}
Shortcut {
sequence: "Escape"
onActivated: root . closeTopmost ( )
}
Shortcut {
sequence: "F5"
onActivated: App . importAllGames ( )
}
Shortcut {
sequence: "Ctrl+H"
onActivated: {
if ( root . currentSource === "hidden" ) {
root . currentSource = "all"
} else {
root . currentSource = "hidden"
}
}
}
Shortcut {
sequence: "Ctrl+D"
onActivated: {
if ( root . selectedGame ) {
detailsSheet . open ( )
}
}
}
2026-01-24 12:27:30 +00:00
Shortcut {
sequence: "F9"
onActivated: root . cycleUiMode ( )
}
2026-01-18 12:13:07 +00:00
Connections {
target: GamepadManager
function onBackPressed ( ) {
root . closeTopmost ( )
}
2026-01-24 12:27:30 +00:00
function onLeftBumperPressed ( ) {
2026-01-24 19:50:20 +00:00
if ( detailsSheet . opened || gameEditDialog . visible || importSheet . opened || settingsLayerOpen || aboutSheet . opened ) return
2026-01-24 12:27:30 +00:00
if ( root . hintContext !== "library" && root . hintContext !== "sidebar" ) return
sidebarView . cycleSource ( - 1 )
}
function onRightBumperPressed ( ) {
2026-01-24 19:50:20 +00:00
if ( detailsSheet . opened || gameEditDialog . visible || importSheet . opened || settingsLayerOpen || aboutSheet . opened ) return
2026-01-24 12:27:30 +00:00
if ( root . hintContext !== "library" && root . hintContext !== "sidebar" ) return
sidebarView . cycleSource ( 1 )
}
function onDetailsPressed ( ) {
2026-01-24 19:50:20 +00:00
if ( gameEditDialog . visible || importSheet . opened || settingsLayerOpen || aboutSheet . opened || ( sidebar . modal && sidebar . opened ) ) return
2026-01-24 12:27:30 +00:00
if ( detailsSheet . opened ) {
if ( root . selectedGame ) {
root . selectedGame . favorite = ! root . selectedGame . favorite
}
return
}
if ( ! libraryView || ! libraryView . visible ) return
libraryView . openDetailsForFocusedGame ( )
}
2026-01-18 12:13:07 +00:00
function onMenuPressed ( ) {
2026-01-24 19:50:20 +00:00
if ( ! settingsLayerOpen ) {
2026-01-18 12:13:07 +00:00
if ( root . canOpenSettings ( ) ) {
2026-01-24 19:50:20 +00:00
root . openSettings ( )
2026-01-24 12:27:30 +00:00
} else if ( detailsSheet . opened ) {
detailsSheet . close ( )
2026-01-24 19:50:20 +00:00
Qt . callLater ( function ( ) { root . openSettings ( ) } )
2026-01-18 12:13:07 +00:00
} else {
root . closeTopmost ( )
}
} else {
2026-01-24 19:50:20 +00:00
root . closeSettings ( )
2026-01-18 12:13:07 +00:00
}
}
function onSearchPressed ( ) {
2026-01-24 12:27:30 +00:00
if ( detailsSheet . opened ) {
detailsSheet . editRequested ( )
return
}
2026-01-24 19:50:20 +00:00
if ( gameEditDialog . visible || importSheet . opened || settingsLayerOpen || aboutSheet . opened || ( sidebar . modal && sidebar . opened ) ) return
2026-01-18 12:13:07 +00:00
searchActive = true
Qt . callLater ( function ( ) { libraryView . focusSearch ( ) } )
}
}
Connections {
target: root . pageStack . layers
function onDepthChanged ( ) {
if ( root . pageStack . layers . depth <= 1 ) {
2026-01-24 19:50:20 +00:00
root . settingsLayerOpen = false
2026-01-18 12:13:07 +00:00
Qt . callLater ( function ( ) { libraryView . restoreFocus ( ) } )
}
}
}
globalDrawer: Kirigami . OverlayDrawer {
id: sidebar
edge: Qt . LeftEdge
modal: root . isMobile || root . isNarrowScreen
handleClosedIcon.source: "application-menu"
handleOpenIcon.source: "go-previous"
handleVisible: root . isMobile || root . isNarrowScreen
width: {
if ( root . isMobile ) return Math . min ( root . width * 0.85 , Kirigami . Units . gridUnit * 20 )
if ( root . isTablet ) return Kirigami . Units . gridUnit * 16
return Kirigami . Units . gridUnit * 14
}
Binding {
target: sidebar
property: "drawerOpen"
value: true
when: ! sidebar . modal
}
onOpened: sidebarView . focusList ( )
onClosed: Qt . callLater ( function ( ) { libraryView . restoreFocus ( ) } )
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
contentItem: ColumnLayout {
spacing: 0
Kirigami . AbstractApplicationHeader {
Layout.fillWidth: true
contentItem: Item {
anchors.fill: parent
anchors.margins: Kirigami . Units . largeSpacing
RowLayout {
anchors.fill: parent
spacing: Kirigami . Units . mediumSpacing
IconWithResourceFallback {
primary: "org.kde.alakarte"
secondary: ""
resourceFallback: Qt . resolvedUrl ( "icons/app/org.kde.alakarte.svg" )
mask: false
Layout.preferredWidth: Kirigami . Units . iconSizes . medium
Layout.preferredHeight: Kirigami . Units . iconSizes . medium
}
Kirigami . Heading {
text: i18n ( "A-La-Karte" )
level: 2
Layout.fillWidth: true
}
}
}
}
SidebarView {
id: sidebarView
Layout.fillWidth: true
Layout.fillHeight: true
onSourceSelected: function ( source ) {
root . currentSource = source
if ( sidebar . modal ) {
2026-01-24 12:27:30 +00:00
if ( ! sidebarView . suppressAutoClose ) {
sidebar . close ( )
Qt . callLater ( function ( ) { libraryView . restoreFocus ( ) } )
}
2026-01-18 12:13:07 +00:00
}
}
onSettingsRequested: {
if ( sidebar . modal ) {
sidebar . close ( )
}
2026-01-24 19:50:20 +00:00
root . openSettings ( )
2026-01-18 12:13:07 +00:00
}
onImportRequested: {
if ( sidebar . modal ) {
sidebar . close ( )
}
importSheet . open ( )
}
onAboutRequested: {
if ( sidebar . modal ) {
sidebar . close ( )
}
2026-01-19 23:15:28 +00:00
aboutSheet . open ( )
2026-01-18 12:13:07 +00:00
}
}
}
}
pageStack.initialPage: Kirigami . Page {
id: mainPage
title: sidebarView . currentSourceName
padding: Kirigami . Units . largeSpacing
actions: [
Kirigami . Action {
icon.name: "search"
text: i18n ( "Search" )
checkable: true
checked: root . searchActive
onTriggered: {
root . searchActive = ! root . searchActive
if ( root . searchActive ) {
Qt . callLater ( function ( ) { libraryView . focusSearch ( ) } )
} else {
2026-01-24 12:27:30 +00:00
libraryView . clearSearch ( )
2026-01-18 12:13:07 +00:00
libraryView . restoreFocus ( )
}
}
} ,
Kirigami . Action {
icon.name: "list-add"
text: i18n ( "Add Game" )
onTriggered: {
gameEditDialog . game = null
gameEditDialog . open ( )
}
} ,
Kirigami . Action {
icon.name: "document-import"
text: i18n ( "Import Games" )
2026-01-24 12:27:30 +00:00
visible: sidebar . modal
2026-01-18 12:13:07 +00:00
onTriggered: importSheet . open ( )
} ,
Kirigami . Action {
icon.name: "configure"
text: i18n ( "Settings" )
2026-01-24 12:27:30 +00:00
visible: sidebar . modal
2026-01-24 19:50:20 +00:00
onTriggered: root . openSettings ( )
2026-01-18 12:13:07 +00:00
}
]
LibraryView {
id: libraryView
anchors.fill: parent
filterSource: root . currentSource
searchActive: root . searchActive
adaptiveCardSize: root . adaptiveCardSize
isTouchDevice: root . isTouchDevice
onGameSelected: function ( game ) {
root . selectedGame = game
detailsSheet . open ( )
}
onGameLaunched: function ( game ) {
App . launcher . launchGame ( game )
}
}
}
GameDetailsSheet {
id: detailsSheet
game: root . selectedGame
onLaunchRequested: {
App . launcher . launchGame ( root . selectedGame )
}
onEditRequested: {
detailsSheet . close ( )
gameEditDialog . game = root . selectedGame
gameEditDialog . open ( )
}
onRemoveRequested: {
if ( root . selectedGame ) {
let gameId = root . selectedGame . id
let gameName = root . selectedGame . name
App . removeGame ( root . selectedGame )
detailsSheet . close ( )
root . selectedGame = null
showPassiveNotification (
i18n ( "%1 removed" , gameName ) ,
"long" ,
i18n ( "Undo" ) ,
function ( ) { App . restoreGame ( gameId ) }
)
}
}
onClosed: {
libraryView . restoreFocus ( )
}
}
Kirigami . OverlaySheet {
id: importSheet
title: i18n ( "Import Games" )
implicitWidth: {
if ( root . isMobile ) return applicationWindow ( ) . width
if ( root . isNarrowScreen ) return applicationWindow ( ) . width - Kirigami . Units . largeSpacing * 2
return Math . min ( applicationWindow ( ) . width - Kirigami . Units . gridUnit * 2 , Kirigami . Units . gridUnit * 32 )
}
implicitHeight: {
if ( root . isMobile ) return applicationWindow ( ) . height
return Math . min ( applicationWindow ( ) . height - Kirigami . Units . gridUnit * 2 , importContent . implicitHeight + Kirigami . Units . gridUnit * 2 )
}
onOpened: steamImportButton . forceActiveFocus ( )
onClosed: libraryView . restoreFocus ( )
function isDescendant ( item , ancestor ) {
let p = item
while ( p ) {
if ( p === ancestor ) return true
2026-01-24 12:27:30 +00:00
if ( ancestor . contentItem && p === ancestor . contentItem ) return true
if ( ancestor . header && p === ancestor . header ) return true
2026-01-18 12:13:07 +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 ( importSheet . isDescendant ( next , importContent ) ) {
next . forceActiveFocus ( )
importScroll . ensureItemVisible ( next )
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 . hasOwnProperty ( "checked" ) ) {
item . checked = ! item . checked
return
}
if ( typeof item . clicked === "function" ) {
item . clicked ( )
return
}
}
Connections {
target: GamepadManager
function onNavigateUp ( ) {
if ( ! importSheet . opened || importScroll . activeFocus ) return
2026-01-24 12:27:30 +00:00
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! importSheet . isDescendant ( w . activeFocusItem , importSheet ) ) return
2026-01-18 12:13:07 +00:00
importSheet . focusNextInChain ( false )
}
function onNavigateDown ( ) {
if ( ! importSheet . opened || importScroll . activeFocus ) return
2026-01-24 12:27:30 +00:00
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! importSheet . isDescendant ( w . activeFocusItem , importSheet ) ) return
importSheet . focusNextInChain ( true )
}
function onNavigateLeft ( ) {
if ( ! importSheet . opened || importScroll . activeFocus ) return
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! importSheet . isDescendant ( w . activeFocusItem , importSheet ) ) return
importSheet . focusNextInChain ( false )
}
function onNavigateRight ( ) {
if ( ! importSheet . opened || importScroll . activeFocus ) return
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! importSheet . isDescendant ( w . activeFocusItem , importSheet ) ) return
2026-01-18 12:13:07 +00:00
importSheet . focusNextInChain ( true )
}
function onSelectPressed ( ) {
if ( ! importSheet . opened || importScroll . activeFocus ) return
2026-01-24 12:27:30 +00:00
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! importSheet . isDescendant ( w . activeFocusItem , importSheet ) ) return
2026-01-18 12:13:07 +00:00
importSheet . activateFocused ( )
}
}
contentItem: QQC2 . ScrollView {
id: importScroll
clip: true
leftPadding: Kirigami . Units . largeSpacing
rightPadding: Kirigami . Units . largeSpacing
topPadding: Kirigami . Units . largeSpacing
bottomPadding: Kirigami . Units . largeSpacing
QQC2.ScrollBar.horizontal.policy: QQC2 . ScrollBar . AlwaysOff
function ensureItemVisible ( item ) {
if ( ! item || ! importScroll . contentItem || ! importScroll . contentItem . contentItem ) return
let flick = importScroll . contentItem
let content = flick . contentItem
let p = item . mapToItem ( content , 0 , 0 )
let itemTop = p . y
let itemBottom = p . y + item . height
let top = flick . contentY
let bottom = flick . contentY + flick . height
if ( itemTop < top ) {
flick . contentY = Math . max ( 0 , itemTop )
} else if ( itemBottom > bottom ) {
flick . contentY = Math . max ( 0 , itemBottom - flick . height )
}
}
function scrollBy ( delta ) {
if ( ! importScroll . contentItem ) return
let maxY = Math . max ( 0 , importScroll . contentItem . contentHeight - importScroll . contentItem . height )
importScroll . contentItem . contentY = Math . max ( 0 , Math . min ( maxY , importScroll . contentItem . contentY + delta ) )
}
Connections {
target: GamepadManager
function onNavigateUp ( ) { if ( importScroll . activeFocus ) importScroll . scrollBy ( - Kirigami . Units . gridUnit * 2 ) }
function onNavigateDown ( ) { if ( importScroll . activeFocus ) importScroll . scrollBy ( Kirigami . Units . gridUnit * 2 ) }
}
ColumnLayout {
id: importContent
width: importScroll . availableWidth
spacing: Kirigami . Units . mediumSpacing
Kirigami . InlineMessage {
Layout.fillWidth: true
type: Kirigami . MessageType . Information
text: App . importStatus
visible: App . importing
}
FormCard . FormCard {
Layout.fillWidth: true
FormCard . FormButtonDelegate {
id: steamImportButton
text: i18n ( "Steam" )
description: i18n ( "Import from Steam library" )
icon.name: ""
leading: IconWithResourceFallback {
primary: "com.valvesoftware.Steam"
secondary: "steam"
resourceFallback: Qt . resolvedUrl ( "icons/brand/steam-symbolic.svg" )
}
2026-01-24 12:27:30 +00:00
enabled: ! App . importing && App . config . importSteam && ! App . gameModel . hasPlatformPrefix ( "Steam" ) && App . gameModel . count >= 0
2026-01-18 12:13:07 +00:00
onClicked: App . importFromSteam ( )
}
FormCard . FormDelegateSeparator { }
FormCard . FormButtonDelegate {
text: i18n ( "Lutris" )
description: i18n ( "Import from Lutris" )
icon.name: ""
leading: IconWithResourceFallback {
primary: "lutris"
secondary: "applications-games"
}
2026-01-24 12:27:30 +00:00
enabled: ! App . importing && App . config . importLutris && ! App . gameModel . hasPlatformPrefix ( "Lutris" ) && App . gameModel . count >= 0
2026-01-18 12:13:07 +00:00
onClicked: App . importFromLutris ( )
}
FormCard . FormDelegateSeparator { }
FormCard . FormButtonDelegate {
text: i18n ( "Heroic" )
description: i18n ( "Epic, GOG, Amazon games" )
icon.name: ""
leading: IconWithResourceFallback {
primary: "com.heroicgameslauncher.hgl"
secondary: "applications-games"
}
2026-01-24 12:27:30 +00:00
enabled: ! App . importing && App . config . importHeroic && ! App . gameModel . hasPlatformPrefix ( "Heroic" ) && App . gameModel . count >= 0
2026-01-18 12:13:07 +00:00
onClicked: App . importFromHeroic ( )
}
FormCard . FormDelegateSeparator { }
FormCard . FormButtonDelegate {
text: i18n ( "Desktop Entries" )
description: i18n ( "Games from system .desktop files" )
icon.name: ""
leading: IconWithResourceFallback {
primary: "user-desktop"
secondary: "computer"
}
2026-01-24 12:27:30 +00:00
enabled: ! App . importing && App . config . importDesktop && ! App . gameModel . hasPlatformPrefix ( "Desktop" ) && App . gameModel . count >= 0
2026-01-18 12:13:07 +00:00
onClicked: App . importFromDesktop ( )
}
FormCard . FormDelegateSeparator { }
FormCard . FormButtonDelegate {
text: i18n ( "Bottles" )
description: i18n ( "Wine applications from Bottles" )
icon.name: ""
leading: IconWithResourceFallback {
primary: "com.usebottles.bottles"
secondary: "application-x-executable"
}
2026-01-24 12:27:30 +00:00
enabled: ! App . importing && App . config . importBottles && ! App . gameModel . hasPlatformPrefix ( "Bottles" ) && App . gameModel . count >= 0
2026-01-18 12:13:07 +00:00
onClicked: App . importFromBottles ( )
}
FormCard . FormDelegateSeparator { }
FormCard . FormButtonDelegate {
text: i18n ( "Flatpak Games" )
description: i18n ( "Games installed via Flatpak" )
icon.name: ""
leading: IconWithResourceFallback {
primary: "flatpak-discover"
secondary: "applications-games"
}
2026-01-24 12:27:30 +00:00
enabled: ! App . importing && App . config . importFlatpak && ! App . gameModel . hasPlatformPrefix ( "Flatpak" ) && App . gameModel . count >= 0
2026-01-18 12:13:07 +00:00
onClicked: App . importFromFlatpak ( )
}
FormCard . FormDelegateSeparator { }
FormCard . FormButtonDelegate {
text: i18n ( "itch.io" )
description: i18n ( "Games from itch.io app" )
icon.name: ""
leading: IconWithResourceFallback {
primary: "io.itch.itch"
secondary: "itch"
resourceFallback: Qt . resolvedUrl ( "icons/brand/itchdotio-symbolic.svg" )
}
2026-01-24 12:27:30 +00:00
enabled: ! App . importing && App . config . importItch && ! App . gameModel . hasPlatformPrefix ( "itch.io" ) && App . gameModel . count >= 0
2026-01-18 12:13:07 +00:00
onClicked: App . importFromItch ( )
}
FormCard . FormDelegateSeparator { }
FormCard . FormButtonDelegate {
text: i18n ( "Legendary" )
description: i18n ( "Epic Games via Legendary CLI" )
icon.name: ""
leading: IconWithResourceFallback {
primary: "legendary"
secondary: "applications-games"
}
2026-01-24 12:27:30 +00:00
enabled: ! App . importing && App . config . importLegendary && ! App . gameModel . hasPlatformPrefix ( "Legendary" ) && App . gameModel . count >= 0
2026-01-18 12:13:07 +00:00
onClicked: App . importFromLegendary ( )
}
FormCard . FormDelegateSeparator { }
FormCard . FormButtonDelegate {
text: i18n ( "RetroArch" )
description: i18n ( "Games from RetroArch playlists" )
icon.name: ""
leading: IconWithResourceFallback {
primary: "org.libretro.RetroArch"
secondary: "retroarch"
resourceFallback: Qt . resolvedUrl ( "icons/brand/retroarch-symbolic.svg" )
}
2026-01-24 12:27:30 +00:00
enabled: ! App . importing && App . config . importRetroArch && ! App . gameModel . hasPlatformPrefix ( "RetroArch" ) && App . gameModel . count >= 0
2026-01-18 12:13:07 +00:00
onClicked: App . importFromRetroArch ( )
}
}
RowLayout {
Layout.alignment: Qt . AlignRight
spacing: Kirigami . Units . mediumSpacing
QQC2 . Button {
text: i18n ( "Import All" )
icon.name: "document-import"
2026-01-24 12:27:30 +00:00
enabled: ! App . importing && App . gameModel . count >= 0 && ( ( App . config . importSteam && ! App . gameModel . hasPlatformPrefix ( "Steam" ) )
|| ( App . config . importLutris && ! App . gameModel . hasPlatformPrefix ( "Lutris" ) )
|| ( App . config . importHeroic && ! App . gameModel . hasPlatformPrefix ( "Heroic" ) )
|| ( App . config . importDesktop && ! App . gameModel . hasPlatformPrefix ( "Desktop" ) )
|| ( App . config . importBottles && ! App . gameModel . hasPlatformPrefix ( "Bottles" ) )
|| ( App . config . importFlatpak && ! App . gameModel . hasPlatformPrefix ( "Flatpak" ) )
|| ( App . config . importItch && ! App . gameModel . hasPlatformPrefix ( "itch.io" ) )
|| ( App . config . importLegendary && ! App . gameModel . hasPlatformPrefix ( "Legendary" ) )
|| ( App . config . importRetroArch && ! App . gameModel . hasPlatformPrefix ( "RetroArch" ) ) )
2026-01-18 12:13:07 +00:00
onClicked: App . importAllGames ( )
}
QQC2 . Button {
text: i18n ( "Close" )
onClicked: importSheet . close ( )
}
}
}
}
}
2026-01-24 19:50:20 +00:00
Component {
id: settingsLayerComponent
Kirigami . Page {
id: settingsLayerPage
title: i18n ( "Settings" )
padding: 0
actions: [
Kirigami . Action {
icon.name: "go-previous"
text: i18n ( "Close" )
onTriggered: root . closeSettings ( )
}
]
Component.onCompleted: {
Qt . callLater ( function ( ) {
settingsLayerContent . focusFirstControl ( )
} )
}
function isDescendant ( item , ancestor ) {
let p = item
while ( p ) {
if ( p === 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 ( settingsLayerPage . isDescendant ( next , settingsLayerPage ) ) {
next . forceActiveFocus ( )
if ( settingsLayerPage . isDescendant ( next , settingsLayerContent ) ) {
settingsLayerScroll . ensureItemVisible ( next )
}
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 . hasOwnProperty ( "checked" ) ) {
item . checked = ! item . checked
return
}
if ( typeof item . clicked === "function" ) {
item . clicked ( )
return
}
}
Connections {
target: GamepadManager
function onNavigateUp ( ) {
if ( ! root . settingsLayerOpen || settingsLayerScroll . activeFocus ) return
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! settingsLayerPage . isDescendant ( w . activeFocusItem , settingsLayerPage ) ) return
settingsLayerPage . focusNextInChain ( false )
}
function onNavigateDown ( ) {
if ( ! root . settingsLayerOpen || settingsLayerScroll . activeFocus ) return
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! settingsLayerPage . isDescendant ( w . activeFocusItem , settingsLayerPage ) ) return
settingsLayerPage . focusNextInChain ( true )
}
function onSelectPressed ( ) {
if ( ! root . settingsLayerOpen || settingsLayerScroll . activeFocus ) return
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! settingsLayerPage . isDescendant ( w . activeFocusItem , settingsLayerPage ) ) return
settingsLayerPage . activateFocused ( )
}
}
QQC2 . ScrollView {
id: settingsLayerScroll
anchors.fill: parent
clip: true
readonly property int horizontalContentPadding: root . isMobile ? Kirigami.Units.largeSpacing : Kirigami . Units . gridUnit
readonly property int verticalContentPadding: root . isMobile ? Kirigami.Units.largeSpacing : Kirigami . Units . gridUnit
leftPadding: horizontalContentPadding
rightPadding: horizontalContentPadding
topPadding: verticalContentPadding
bottomPadding: verticalContentPadding
QQC2.ScrollBar.horizontal.policy: QQC2 . ScrollBar . AlwaysOff
function ensureItemVisible ( item ) {
if ( ! item || ! settingsLayerScroll . contentItem || ! settingsLayerScroll . contentItem . contentItem ) return
let flick = settingsLayerScroll . contentItem
let content = flick . contentItem
let p = item . mapToItem ( content , 0 , 0 )
let itemTop = p . y
let itemBottom = p . y + item . height
let top = flick . contentY
let bottom = flick . contentY + flick . height
if ( itemTop < top ) {
flick . contentY = Math . max ( 0 , itemTop )
} else if ( itemBottom > bottom ) {
flick . contentY = Math . max ( 0 , itemBottom - flick . height )
}
}
function scrollBy ( delta ) {
if ( ! settingsLayerScroll . contentItem ) return
let maxY = Math . max ( 0 , settingsLayerScroll . contentItem . contentHeight - settingsLayerScroll . contentItem . height )
settingsLayerScroll . contentItem . contentY = Math . max ( 0 , Math . min ( maxY , settingsLayerScroll . contentItem . contentY + delta ) )
}
Connections {
target: GamepadManager
function onNavigateUp ( ) { if ( root . settingsLayerOpen && settingsLayerScroll . activeFocus ) settingsLayerScroll . scrollBy ( - Kirigami . Units . gridUnit * 2 ) }
function onNavigateDown ( ) { if ( root . settingsLayerOpen && settingsLayerScroll . activeFocus ) settingsLayerScroll . scrollBy ( Kirigami . Units . gridUnit * 2 ) }
}
SettingsPage {
id: settingsLayerContent
width: settingsLayerScroll . availableWidth
}
}
}
}
2026-01-18 12:13:07 +00:00
Kirigami . OverlaySheet {
id: settingsSheet
title: i18n ( "Settings" )
2026-01-24 19:50:20 +00:00
parent: applicationWindow ( ) . overlay
readonly property Item contentArea: applicationWindow ( ) . pageStack
readonly property int sideMargin: root . isMobile ? 0 : Kirigami . Units . gridUnit * 2
readonly property int verticalMargin: root . isMobile ? 0 : Kirigami . Units . gridUnit
readonly property point contentAreaPos: {
if ( ! settingsSheet . contentArea ) return Qt . point ( 0 , 0 )
return settingsSheet . contentArea . mapToItem ( applicationWindow ( ) . overlay , 0 , 0 )
}
readonly property int usableLeft: settingsSheet . contentAreaPos . x
readonly property int usableTop: settingsSheet . contentAreaPos . y
readonly property int usableWidth: settingsSheet . contentArea ? settingsSheet.contentArea.width : applicationWindow ( ) . width
readonly property int usableHeight: settingsSheet . contentArea ? settingsSheet.contentArea.height : applicationWindow ( ) . height
readonly property int sheetAvailableWidth: Math . max ( 0 , settingsSheet . usableWidth - settingsSheet . sideMargin * 2 )
readonly property int sheetAvailableHeight: Math . max ( 0 , settingsSheet . usableHeight - settingsSheet . verticalMargin * 2 )
2026-01-19 23:15:28 +00:00
closePolicy: QQC2 . Popup . CloseOnEscape | QQC2 . Popup . CloseOnPressOutside
2026-01-24 19:50:20 +00:00
width: {
if ( root . isMobile ) return settingsSheet . sheetAvailableWidth
if ( root . isNarrowScreen ) return settingsSheet . sheetAvailableWidth
let desired = Math . round ( settingsSheet . sheetAvailableWidth * 0.82 )
return Math . min ( settingsSheet . sheetAvailableWidth , Math . max ( Kirigami . Units . gridUnit * 44 , Math . min ( desired , Kirigami . Units . gridUnit * 96 ) ) )
2026-01-18 12:13:07 +00:00
}
2026-01-24 19:50:20 +00:00
height: {
if ( root . isMobile ) return settingsSheet . sheetAvailableHeight
let contentH = settingsContent . implicitHeight + settingsScroll . topPadding + settingsScroll . bottomPadding + Kirigami . Units . gridUnit * 4
return Math . max ( Math . round ( settingsSheet . sheetAvailableHeight * 0.85 ) , Math . min ( contentH , settingsSheet . sheetAvailableHeight ) )
}
x: {
return settingsSheet . usableLeft + Math . round ( ( settingsSheet . usableWidth - width ) / 2 )
}
y: {
return settingsSheet . usableTop + settingsSheet . verticalMargin
2026-01-18 12:13:07 +00:00
}
onOpened: settingsContent . focusFirstControl ( )
onClosed: libraryView . restoreFocus ( )
2026-01-19 23:15:28 +00:00
header: Kirigami . ShadowedRectangle {
id: settingsHeader
2026-01-24 19:50:20 +00:00
implicitWidth: settingsSheet . width
2026-01-19 23:15:28 +00:00
implicitHeight: settingsHeaderRow . implicitHeight + Kirigami . Units . largeSpacing * 2
radius: Kirigami . Units . mediumSpacing
color: Kirigami . Theme . backgroundColor
shadow {
size: Kirigami . Units . smallSpacing
color: Qt . rgba ( 0 , 0 , 0 , 0.20 )
}
RowLayout {
id: settingsHeaderRow
anchors.fill: parent
anchors.margins: Kirigami . Units . largeSpacing
spacing: Kirigami . Units . mediumSpacing
Kirigami . Heading {
text: settingsSheet . title
level: 2
Layout.fillWidth: true
elide: Text . ElideRight
}
QQC2 . ToolButton {
text: i18n ( "Close" )
icon.name: "dialog-close"
display: QQC2 . AbstractButton . IconOnly
onClicked: settingsSheet . close ( )
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: text
}
}
}
2026-01-18 12:13:07 +00:00
function isDescendant ( item , ancestor ) {
let p = item
while ( p ) {
if ( p === ancestor ) return true
2026-01-24 12:27:30 +00:00
if ( ancestor . contentItem && p === ancestor . contentItem ) return true
if ( ancestor . header && p === ancestor . header ) return true
2026-01-18 12:13:07 +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
2026-01-19 23:15:28 +00:00
if ( settingsSheet . isDescendant ( next , settingsSheet ) ) {
2026-01-18 12:13:07 +00:00
next . forceActiveFocus ( )
2026-01-19 23:15:28 +00:00
if ( settingsSheet . isDescendant ( next , settingsContent ) ) {
settingsScroll . ensureItemVisible ( next )
}
2026-01-18 12:13:07 +00:00
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 . hasOwnProperty ( "checked" ) ) {
item . checked = ! item . checked
return
}
if ( typeof item . clicked === "function" ) {
item . clicked ( )
return
}
}
Connections {
target: GamepadManager
function onNavigateUp ( ) {
if ( ! settingsSheet . opened || settingsScroll . activeFocus ) return
settingsSheet . focusNextInChain ( false )
}
function onNavigateDown ( ) {
if ( ! settingsSheet . opened || settingsScroll . activeFocus ) return
settingsSheet . focusNextInChain ( true )
}
function onSelectPressed ( ) {
if ( ! settingsSheet . opened || settingsScroll . activeFocus ) return
settingsSheet . activateFocused ( )
}
}
contentItem: QQC2 . ScrollView {
id: settingsScroll
clip: true
2026-01-24 19:50:20 +00:00
readonly property int horizontalContentPadding: root . isMobile ? Kirigami.Units.largeSpacing : Kirigami . Units . gridUnit
readonly property int verticalContentPadding: root . isMobile ? Kirigami.Units.largeSpacing : Kirigami . Units . gridUnit
leftPadding: horizontalContentPadding
rightPadding: horizontalContentPadding
topPadding: verticalContentPadding
bottomPadding: verticalContentPadding
2026-01-18 12:13:07 +00:00
QQC2.ScrollBar.horizontal.policy: QQC2 . ScrollBar . AlwaysOff
function ensureItemVisible ( item ) {
if ( ! item || ! settingsScroll . contentItem || ! settingsScroll . contentItem . contentItem ) return
let flick = settingsScroll . contentItem
let content = flick . contentItem
let p = item . mapToItem ( content , 0 , 0 )
let itemTop = p . y
let itemBottom = p . y + item . height
let top = flick . contentY
let bottom = flick . contentY + flick . height
if ( itemTop < top ) {
flick . contentY = Math . max ( 0 , itemTop )
} else if ( itemBottom > bottom ) {
flick . contentY = Math . max ( 0 , itemBottom - flick . height )
}
}
function scrollBy ( delta ) {
if ( ! settingsScroll . contentItem ) return
let maxY = Math . max ( 0 , settingsScroll . contentItem . contentHeight - settingsScroll . contentItem . height )
settingsScroll . contentItem . contentY = Math . max ( 0 , Math . min ( maxY , settingsScroll . contentItem . contentY + delta ) )
}
Connections {
target: GamepadManager
function onNavigateUp ( ) { if ( settingsScroll . activeFocus ) settingsScroll . scrollBy ( - Kirigami . Units . gridUnit * 2 ) }
function onNavigateDown ( ) { if ( settingsScroll . activeFocus ) settingsScroll . scrollBy ( Kirigami . Units . gridUnit * 2 ) }
}
SettingsPage {
id: settingsContent
width: settingsScroll . availableWidth
}
}
}
2026-01-19 23:15:28 +00:00
Kirigami . OverlaySheet {
id: aboutSheet
title: i18n ( "About" )
closePolicy: QQC2 . Popup . CloseOnEscape | QQC2 . Popup . CloseOnPressOutside
implicitWidth: {
if ( root . isMobile ) return applicationWindow ( ) . width
if ( root . isNarrowScreen ) return applicationWindow ( ) . width - Kirigami . Units . largeSpacing * 2
return Math . min ( applicationWindow ( ) . width - Kirigami . Units . gridUnit * 2 , Kirigami . Units . gridUnit * 30 )
}
implicitHeight: {
if ( root . isMobile ) return applicationWindow ( ) . height
return Math . min ( applicationWindow ( ) . height - Kirigami . Units . gridUnit * 2 , Kirigami . Units . gridUnit * 42 )
}
2026-01-24 12:27:30 +00:00
onOpened: aboutCloseButton . forceActiveFocus ( )
2026-01-19 23:15:28 +00:00
onClosed: libraryView . restoreFocus ( )
2026-01-24 12:27:30 +00:00
function isDescendant ( item , ancestor ) {
let p = item
while ( p ) {
if ( p === ancestor ) return true
if ( ancestor . contentItem && p === ancestor . contentItem ) return true
if ( ancestor . header && p === ancestor . header ) 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 ( aboutSheet . isDescendant ( next , aboutSheet ) ) {
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 . hasOwnProperty ( "checked" ) ) {
item . checked = ! item . checked
return
}
if ( typeof item . clicked === "function" ) {
item . clicked ( )
return
}
}
Connections {
target: GamepadManager
function onNavigateUp ( ) {
if ( ! aboutSheet . opened ) return
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! aboutSheet . isDescendant ( w . activeFocusItem , aboutSheet ) ) return
aboutSheet . focusNextInChain ( false )
}
function onNavigateDown ( ) {
if ( ! aboutSheet . opened ) return
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! aboutSheet . isDescendant ( w . activeFocusItem , aboutSheet ) ) return
aboutSheet . focusNextInChain ( true )
}
function onNavigateLeft ( ) {
if ( ! aboutSheet . opened ) return
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! aboutSheet . isDescendant ( w . activeFocusItem , aboutSheet ) ) return
aboutSheet . focusNextInChain ( false )
}
function onNavigateRight ( ) {
if ( ! aboutSheet . opened ) return
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! aboutSheet . isDescendant ( w . activeFocusItem , aboutSheet ) ) return
aboutSheet . focusNextInChain ( true )
}
function onSelectPressed ( ) {
if ( ! aboutSheet . opened ) return
let w = applicationWindow ( )
if ( ! w || ! w . activeFocusItem ) return
if ( ! aboutSheet . isDescendant ( w . activeFocusItem , aboutSheet ) ) return
aboutSheet . activateFocused ( )
}
}
2026-01-19 23:15:28 +00:00
header: Kirigami . ShadowedRectangle {
id: aboutHeader
implicitWidth: aboutSheet . implicitWidth
implicitHeight: aboutHeaderRow . implicitHeight + Kirigami . Units . largeSpacing * 2
radius: Kirigami . Units . mediumSpacing
color: Kirigami . Theme . backgroundColor
shadow {
size: Kirigami . Units . smallSpacing
color: Qt . rgba ( 0 , 0 , 0 , 0.20 )
}
RowLayout {
id: aboutHeaderRow
anchors.fill: parent
anchors.margins: Kirigami . Units . largeSpacing
spacing: Kirigami . Units . mediumSpacing
Kirigami . Heading {
text: aboutSheet . title
level: 2
Layout.fillWidth: true
elide: Text . ElideRight
}
QQC2 . ToolButton {
2026-01-24 12:27:30 +00:00
id: aboutCloseButton
2026-01-19 23:15:28 +00:00
text: i18n ( "Close" )
icon.name: "dialog-close"
display: QQC2 . AbstractButton . IconOnly
onClicked: aboutSheet . close ( )
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: text
}
}
}
contentItem: Item {
Loader {
id: aboutPageLoader
anchors.fill: parent
active: aboutSheet . opened
sourceComponent: FormCard . AboutPage {
title: i18n ( "About" )
aboutData: ( {
"displayName" : i18n ( "A-La-Karte" ) ,
"componentName" : "alakarte" ,
"shortDescription" : i18n ( "A unified game launcher for KDE Plasma" ) ,
"homepage" : "" ,
"bugAddress" : "" ,
"version" : Qt . application . version ,
"otherText" : "" ,
"authors" : [ ] ,
"credits" : [ ] ,
"translators" : [ ] ,
"licenses" : [
{
"name" : i18n ( "GNU General Public License v3.0 or later" ) ,
"text" : "This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n\nThe full license text is available at:\nhttps://www.gnu.org/licenses/gpl-3.0.txt" ,
"spdx" : "GPL-3.0-or-later"
}
] ,
"copyrightStatement" : "" ,
"desktopFileName" : "org.kde.alakarte"
} )
}
}
}
}
2026-01-18 12:13:07 +00:00
GameEditDialog {
id: gameEditDialog
parent: root . overlay
onClosed: libraryView . restoreFocus ( )
onGameCreated: function ( game ) {
showPassiveNotification ( i18n ( "Added %1" , game . name ) )
}
onGameUpdated: function ( game ) {
showPassiveNotification ( i18n ( "Updated %1" , game . name ) )
}
}
Connections {
target: App
function onImportCompleted ( count ) {
showPassiveNotification ( i18np ( "Imported %1 game" , "Imported %1 games" , count ) )
}
function onImportError ( error ) {
showPassiveNotification ( i18n ( "Import error: %1" , error ) , "long" )
}
}
Connections {
target: App . launcher
function onGameStarted ( game ) {
showPassiveNotification ( i18n ( "Launching %1..." , game . name ) )
}
function onGameError ( game , error ) {
showPassiveNotification ( i18n ( "Error launching %1: %2" , game . name , error ) , "long" )
}
}
Component.onCompleted: {
if ( App . gameModel . count === 0 ) {
importSheet . open ( )
}
}
}