mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-03-27 01:03:09 +00:00
836 lines
28 KiB
QML
836 lines
28 KiB
QML
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||
|
|
|
||
|
|
import QtQuick
|
||
|
|
import QtQuick.Controls as QQC2
|
||
|
|
import QtQuick.Layouts
|
||
|
|
import 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
|
||
|
|
}
|
||
|
|
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
|
||
|
|
&& !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() })
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Component {
|
||
|
|
id: aboutPageComponent
|
||
|
|
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": [
|
||
|
|
{
|
||
|
|
"name": i18n("A-La-Karte Contributors"),
|
||
|
|
"task": i18n("Contributors"),
|
||
|
|
"emailAddress": "",
|
||
|
|
"webAddress": "",
|
||
|
|
"ocsUsername": ""
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"credits": [],
|
||
|
|
"translators": [],
|
||
|
|
"licenses": [
|
||
|
|
{
|
||
|
|
"name": i18n("GNU General Public License v3.0 or later"),
|
||
|
|
"text": "",
|
||
|
|
"spdx": "GPL-3.0-or-later"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"copyrightStatement": i18n("© 2026 A-La-Karte Contributors"),
|
||
|
|
"desktopFileName": "org.kde.alakarte"
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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()
|
||
|
|
}
|
||
|
|
root.pageStack.layers.push(aboutPageComponent)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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")
|
||
|
|
|
||
|
|
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, settingsContent.implicitHeight + Kirigami.Units.gridUnit * 2)
|
||
|
|
}
|
||
|
|
|
||
|
|
onOpened: settingsContent.focusFirstControl()
|
||
|
|
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 (settingsSheet.isDescendant(next, settingsContent)) {
|
||
|
|
next.forceActiveFocus()
|
||
|
|
settingsScroll.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 (!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
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|