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
function closeTopmost ( ) {
if ( gameEditDialog . visible ) {
gameEditDialog . close ( )
return true
}
if ( detailsSheet . opened ) {
detailsSheet . close ( )
return true
}
if ( importSheet . opened ) {
importSheet . close ( )
return true
}
if ( settingsSheet . opened ) {
settingsSheet . 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
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-18 12:13:07 +00:00
&& ! sidebar . opened
&& root . pageStack . layers . depth <= 1
}
readonly property bool isMobile: Kirigami . Settings . isMobile
readonly property bool isTablet: Kirigami . Settings . tabletMode && ! Kirigami . Settings . isMobile
readonly property bool isTouchDevice: Kirigami . Settings . tabletMode || Kirigami . Settings . isMobile
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
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
onActivated: settingsSheet . open ( )
}
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 ( )
}
}
}
Connections {
target: GamepadManager
function onBackPressed ( ) {
root . closeTopmost ( )
}
function onMenuPressed ( ) {
if ( ! settingsSheet . opened ) {
if ( root . canOpenSettings ( ) ) {
settingsSheet . open ( )
} else {
root . closeTopmost ( )
}
} else {
settingsSheet . close ( )
}
}
function onSearchPressed ( ) {
searchActive = true
Qt . callLater ( function ( ) { libraryView . focusSearch ( ) } )
}
}
Connections {
target: root . pageStack . layers
function onDepthChanged ( ) {
if ( root . pageStack . layers . depth <= 1 ) {
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 ) {
sidebar . close ( )
Qt . callLater ( function ( ) { libraryView . restoreFocus ( ) } )
}
}
onSettingsRequested: {
if ( sidebar . modal ) {
sidebar . close ( )
}
settingsSheet . open ( )
}
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 {
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" )
onTriggered: importSheet . open ( )
} ,
Kirigami . Action {
icon.name: "configure"
text: i18n ( "Settings" )
onTriggered: settingsSheet . open ( )
}
]
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
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
importSheet . focusNextInChain ( false )
}
function onNavigateDown ( ) {
if ( ! importSheet . opened || importScroll . activeFocus ) return
importSheet . focusNextInChain ( true )
}
function onSelectPressed ( ) {
if ( ! importSheet . opened || importScroll . activeFocus ) return
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" )
}
enabled: ! App . importing
onClicked: App . importFromSteam ( )
}
FormCard . FormDelegateSeparator { }
FormCard . FormButtonDelegate {
text: i18n ( "Lutris" )
description: i18n ( "Import from Lutris" )
icon.name: ""
leading: IconWithResourceFallback {
primary: "lutris"
secondary: "applications-games"
}
enabled: ! App . importing
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"
}
enabled: ! App . importing
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"
}
enabled: ! App . importing
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"
}
enabled: ! App . importing
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"
}
enabled: ! App . importing
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" )
}
enabled: ! App . importing
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"
}
enabled: ! App . importing
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" )
}
enabled: ! App . importing
onClicked: App . importFromRetroArch ( )
}
}
RowLayout {
Layout.alignment: Qt . AlignRight
spacing: Kirigami . Units . mediumSpacing
QQC2 . Button {
text: i18n ( "Import All" )
icon.name: "document-import"
enabled: ! App . importing
onClicked: App . importAllGames ( )
}
QQC2 . Button {
text: i18n ( "Close" )
onClicked: importSheet . close ( )
}
}
}
}
}
Kirigami . OverlaySheet {
id: settingsSheet
title: i18n ( "Settings" )
2026-01-19 23:15:28 +00:00
closePolicy: QQC2 . Popup . CloseOnEscape | QQC2 . Popup . CloseOnPressOutside
2026-01-18 12:13:07 +00:00
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
2026-01-19 23:15:28 +00:00
return Math . min ( applicationWindow ( ) . height - Kirigami . Units . gridUnit * 2 , Kirigami . Units . gridUnit * 42 )
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
implicitWidth: settingsSheet . implicitWidth
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
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
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 || ! 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 )
}
onClosed: libraryView . restoreFocus ( )
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 {
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 ( )
}
}
}