// 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.alakarte import "components" Item { id: root property string currentSource: "all" signal sourceSelected(string source) signal settingsRequested() signal importRequested() signal focusDownRequested() function focusCurrent() { if (tabList.currentItem) { tabList.currentItem.forceActiveFocus() return } tabList.forceActiveFocus() } function selectNext() { if (sourceModel.count <= 0) return let i = (tabList.currentIndex + 1) % sourceModel.count _applyIndex(i) } function selectPrevious() { if (sourceModel.count <= 0) return let i = tabList.currentIndex - 1 if (i < 0) i = sourceModel.count - 1 _applyIndex(i) } function _applyIndex(i) { tabList.currentIndex = i let item = sourceModel.get(i) if (item) { root.currentSource = item.sourceId root.sourceSelected(item.sourceId) tabList.positionViewAtIndex(i, ListView.Contain) } } function iconInfoForPlatform(platformName) { let p = (platformName || "").toLowerCase() if (p.includes("steam")) return { source: "com.valvesoftware.Steam", fallback: "steam", resourceFallback: Qt.resolvedUrl("icons/brand/steam-symbolic.svg") } if (p.includes("itch")) return { source: "io.itch.itch", fallback: "applications-games", resourceFallback: Qt.resolvedUrl("icons/brand/itchdotio-symbolic.svg") } if (p.includes("retroarch")) return { source: "org.libretro.RetroArch", fallback: "applications-games", resourceFallback: Qt.resolvedUrl("icons/brand/retroarch-symbolic.svg") } if (p.includes("lutris")) return { source: "lutris", fallback: "applications-games", resourceFallback: "" } if (p.includes("heroic")) return { source: "com.heroicgameslauncher.hgl", fallback: "applications-games", resourceFallback: "" } if (p.includes("bottles")) return { source: "com.usebottles.bottles", fallback: "application-x-executable", resourceFallback: "" } if (p.includes("flatpak")) return { source: "flatpak-discover", fallback: "applications-games", resourceFallback: "" } if (p.includes("desktop")) return { source: "user-desktop", fallback: "computer", resourceFallback: "" } if (p.includes("legendary")) return { source: "legendary", fallback: "applications-games", resourceFallback: "" } return { source: "applications-games", fallback: "applications-games", resourceFallback: "" } } onCurrentSourceChanged: { for (let i = 0; i < sourceModel.count; ++i) { if (sourceModel.get(i).sourceId === currentSource) { tabList.currentIndex = i return } } } ListModel { id: sourceModel Component.onCompleted: refresh() function refresh() { clear() let allCount = 0, favCount = 0, hiddenCount = 0 let sources = {} for (let i = 0; i < App.gameModel.rowCount(); i++) { let game = App.gameModel.gameAt(i) if (!game) continue if (game.hidden) { hiddenCount++; continue } allCount++ if (game.favorite) favCount++ let p = game.platform sources[p] = (sources[p] || 0) + 1 } append({ name: i18n("All"), sourceId: "all", icon: "view-list-icons", fallback: "applications-games", resourceFallback: "", count: allCount }) append({ name: i18n("Favorites"), sourceId: "favorites", icon: "bookmark-new", fallback: "bookmark-new", resourceFallback: "", count: favCount }) append({ name: i18n("Hidden"), sourceId: "hidden", icon: "view-hidden", fallback: "view-hidden", resourceFallback: "", count: hiddenCount }) for (let platform in sources) { let info = root.iconInfoForPlatform(platform) append({ name: platform, sourceId: platform, icon: info.source, fallback: info.fallback, resourceFallback: info.resourceFallback, count: sources[platform] }) } for (let j = 0; j < count; ++j) { if (get(j).sourceId === root.currentSource) { tabList.currentIndex = j break } } } } Connections { target: App.gameModel function onCountChanged() { sourceModel.refresh() } } Connections { target: App function onImportCompleted() { sourceModel.refresh() } } RowLayout { anchors.fill: parent spacing: 0 QQC2.ScrollView { Layout.fillWidth: true Layout.fillHeight: true QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff QQC2.ScrollBar.vertical.policy: QQC2.ScrollBar.AlwaysOff ListView { id: tabList model: sourceModel orientation: ListView.Horizontal spacing: Kirigami.Units.smallSpacing leftMargin: Kirigami.Units.largeSpacing rightMargin: Kirigami.Units.largeSpacing topMargin: Kirigami.Units.smallSpacing bottomMargin: Kirigami.Units.smallSpacing clip: true delegate: QQC2.ItemDelegate { id: tabDelegate width: implicitWidth height: tabList.height - tabList.topMargin - tabList.bottomMargin readonly property bool isActive: model.sourceId === root.currentSource readonly property bool isFocused: tabDelegate.activeFocus || tabDelegate.hovered hoverEnabled: true scale: tabDelegate.isFocused ? 1.02 : (tabDelegate.isActive ? 1.01 : 1.0) Behavior on scale { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } background: Kirigami.ShadowedRectangle { radius: Kirigami.Units.smallSpacing color: tabDelegate.isActive ? Kirigami.Theme.highlightColor : Kirigami.Theme.alternateBackgroundColor opacity: tabDelegate.isActive ? 0.20 : (tabDelegate.isFocused ? 0.10 : 0.0) border.width: tabDelegate.isFocused ? 2 : 0 border.color: Kirigami.Theme.highlightColor shadow.size: tabDelegate.isFocused ? Kirigami.Units.mediumSpacing : 0 shadow.color: Qt.rgba(0, 0, 0, 0.28) Rectangle { anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right height: 2 radius: 1 color: Kirigami.Theme.highlightColor visible: tabDelegate.isActive } Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } Behavior on shadow.size { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } Behavior on border.width { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } } leftPadding: Kirigami.Units.mediumSpacing rightPadding: Kirigami.Units.mediumSpacing contentItem: RowLayout { spacing: Kirigami.Units.smallSpacing IconWithResourceFallback { primary: model.icon secondary: model.fallback resourceFallback: model.resourceFallback ? Qt.resolvedUrl(model.resourceFallback) : "" Layout.preferredWidth: Kirigami.Units.iconSizes.small Layout.preferredHeight: Kirigami.Units.iconSizes.small color: tabDelegate.isActive ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor } QQC2.Label { text: model.name color: tabDelegate.isActive ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor font.bold: tabDelegate.isActive elide: Text.ElideRight maximumLineCount: 1 } } onClicked: { root.currentSource = model.sourceId root.sourceSelected(model.sourceId) } Keys.onDownPressed: root.focusDownRequested() Keys.onLeftPressed: { if (tabList.currentIndex > 0) { root._applyIndex(tabList.currentIndex - 1) Qt.callLater(function() { if (tabList.currentItem) tabList.currentItem.forceActiveFocus() }) } } Keys.onRightPressed: { if (tabList.currentIndex < sourceModel.count - 1) { root._applyIndex(tabList.currentIndex + 1) Qt.callLater(function() { if (tabList.currentItem) tabList.currentItem.forceActiveFocus() }) } } } } } Kirigami.Separator { Layout.fillHeight: true } QQC2.ToolButton { id: importBtn icon.name: "document-import" onClicked: root.importRequested() Layout.fillHeight: true hoverEnabled: true scale: activeFocus ? 1.02 : (hovered ? 1.01 : 1.0) Behavior on scale { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } contentItem: Kirigami.Icon { source: "document-import" isMask: true implicitWidth: Kirigami.Units.iconSizes.smallMedium implicitHeight: Kirigami.Units.iconSizes.smallMedium width: implicitWidth height: implicitHeight anchors.centerIn: parent color: "white" } background: Kirigami.ShadowedRectangle { radius: Kirigami.Units.smallSpacing color: Kirigami.Theme.alternateBackgroundColor opacity: parent.activeFocus ? 0.18 : (parent.hovered ? 0.10 : 0.0) border.width: parent.activeFocus ? 2 : 0 border.color: Kirigami.Theme.highlightColor shadow.size: parent.activeFocus ? Kirigami.Units.mediumSpacing : 0 shadow.color: Qt.rgba(0, 0, 0, 0.28) Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } Behavior on shadow.size { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } Behavior on border.width { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } } QQC2.ToolTip.text: i18n("Import Games") QQC2.ToolTip.visible: hovered QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay Keys.onDownPressed: root.focusDownRequested() } QQC2.ToolButton { id: settingsBtn icon.name: "configure" onClicked: root.settingsRequested() Layout.fillHeight: true hoverEnabled: true scale: activeFocus ? 1.02 : (hovered ? 1.01 : 1.0) Behavior on scale { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } contentItem: Kirigami.Icon { source: "configure" isMask: true implicitWidth: Kirigami.Units.iconSizes.smallMedium implicitHeight: Kirigami.Units.iconSizes.smallMedium width: implicitWidth height: implicitHeight anchors.centerIn: parent color: "white" } background: Kirigami.ShadowedRectangle { radius: Kirigami.Units.smallSpacing color: Kirigami.Theme.alternateBackgroundColor opacity: parent.activeFocus ? 0.18 : (parent.hovered ? 0.10 : 0.0) border.width: parent.activeFocus ? 2 : 0 border.color: Kirigami.Theme.highlightColor shadow.size: parent.activeFocus ? Kirigami.Units.mediumSpacing : 0 shadow.color: Qt.rgba(0, 0, 0, 0.28) Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } Behavior on shadow.size { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } Behavior on border.width { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } } QQC2.ToolTip.text: i18n("Settings") QQC2.ToolTip.visible: hovered QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay Keys.onDownPressed: root.focusDownRequested() } } }