From a0b9ea1832d16e792c9a7dcc5f7539fbcc1d616a Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Mon, 23 Mar 2026 10:12:20 +0100 Subject: [PATCH] Integrate MauiKit4/MauiMan4 as submodules, rewrite UI to MAUIkit console shell - Add MauiMan4 + MauiKit4 submodules pinned at v4.0.2 - Wire both into CMake via CMAKE_FIND_PACKAGE_REDIRECTS_DIR build-tree stub - Create MauiMan4 header shim for build-tree include layout - Undefine QT_NO_CAST/QT_NO_FOREACH/QT_NO_KEYWORDS for MauiKit4 targets - Link alakarte against MauiKit4 - Rewrite Main.qml: Maui.ApplicationWindow, dark console shell, blurred game backdrop, category rail, search bar, bottom gamepad hint bar - Rewrite GameCard.qml: cover-art card with scale+glow focus effects - Rewrite LibraryView.qml: Maui.GridBrowser with gamepad D-pad navigation - New ConsoleCategoryRail.qml: horizontal pill category nav with L1/R1 - New ConsoleGameDetail.qml: cinematic full-screen game detail overlay with cover art, action buttons, fade-in animation, gamepad hints --- CMakeLists.txt | 52 + src/CMakeLists.txt | 5 +- src/qml/ConsoleCategoryRail.qml | 161 +++ src/qml/ConsoleGameDetail.qml | 379 +++++ src/qml/GameCard.qml | 403 ++---- src/qml/LibraryView.qml | 375 ++--- src/qml/Main.qml | 2337 ++++--------------------------- third-party/mauikit | 1 + third-party/mauiman | 1 + 9 files changed, 1083 insertions(+), 2631 deletions(-) create mode 100644 src/qml/ConsoleCategoryRail.qml create mode 100644 src/qml/ConsoleGameDetail.qml create mode 160000 third-party/mauikit create mode 160000 third-party/mauiman diff --git a/CMakeLists.txt b/CMakeLists.txt index fb5b58d..4c684dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,6 +107,8 @@ find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS WindowSystem ) +find_package(Qt6 COMPONENTS Multimedia QUIET) + find_package(KF6Auth ${KF_MIN_VERSION} QUIET) find_package(KF6StatusNotifierItem ${KF_MIN_VERSION} QUIET) @@ -115,6 +117,56 @@ find_package(KF6KirigamiAddons 1.0.0 REQUIRED) qt_policy(SET QTP0001 NEW) +set(MAUI_MAJOR_VERSION 4) +set(MAUIMAN_VERSION 4.0.2) +set(MAUIKIT_VERSION 4.0.2) + +set(CMAKE_FIND_PACKAGE_REDIRECTS_DIR "${CMAKE_BINARY_DIR}/package_redirects") +file(MAKE_DIRECTORY "${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}") + +set(BUILD_DEMO OFF CACHE BOOL "" FORCE) +set(BUNDLE_LUV_ICONS OFF CACHE BOOL "" FORCE) + +add_subdirectory(third-party/mauiman EXCLUDE_FROM_ALL) + +set(_mauiman_shim "${CMAKE_BINARY_DIR}/mauiman_include") +file(MAKE_DIRECTORY "${_mauiman_shim}/MauiMan4") +file(COPY "${CMAKE_SOURCE_DIR}/third-party/mauiman/lib/src/modules/" + DESTINATION "${_mauiman_shim}/MauiMan4/" + FILES_MATCHING PATTERN "*.h") +file(COPY "${CMAKE_SOURCE_DIR}/third-party/mauiman/lib/src/mauimanutils.h" + DESTINATION "${_mauiman_shim}/MauiMan4/") +target_include_directories(MauiMan4 PUBLIC + "$") + +file(WRITE "${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/MauiMan4Config.cmake" [=[ +set(MauiMan4_FOUND TRUE) +if(NOT TARGET MauiMan4::MauiMan4) + add_library(MauiMan4::MauiMan4 ALIAS MauiMan4) +endif() +]=]) + +file(WRITE "${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/MauiMan4ConfigVersion.cmake" [=[ +set(PACKAGE_VERSION "4.0.2") +set(PACKAGE_VERSION_EXACT TRUE) +set(PACKAGE_VERSION_COMPATIBLE TRUE) +set(PACKAGE_VERSION_UNSUITABLE FALSE) +]=]) + +add_subdirectory(third-party/mauikit EXCLUDE_FROM_ALL) + +foreach(_mauikit_target MauiKit4 MauiKit4plugin) + if(TARGET ${_mauikit_target}) + target_compile_options(${_mauikit_target} PRIVATE + "-UQT_NO_CAST_FROM_ASCII" + "-UQT_NO_CAST_TO_ASCII" + "-UQT_NO_CAST_FROM_BYTEARRAY" + "-UQT_NO_URL_CAST_FROM_STRING" + "-UQT_NO_FOREACH" + "-UQT_NO_KEYWORDS") + endif() +endforeach() + add_subdirectory(src) add_subdirectory(src/krunner) add_subdirectory(icons) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1bd5df2..02965ab 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -78,6 +78,7 @@ target_link_libraries(alakarte PRIVATE SDL3::SDL3 KF6::Kirigami KF6::I18n + MauiKit4 KF6::CoreAddons KF6::ConfigCore KF6::ConfigGui @@ -104,8 +105,10 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte QML_FILES qml/Main.qml qml/LibraryView.qml + qml/GameCard.qml + qml/ConsoleGameDetail.qml + qml/ConsoleCategoryRail.qml qml/CouchSidebar.qml - qml/GameCard.qml qml/GameDetailsSheet.qml qml/DiagnosticsSheet.qml qml/SettingsPage.qml diff --git a/src/qml/ConsoleCategoryRail.qml b/src/qml/ConsoleCategoryRail.qml new file mode 100644 index 0000000..bc125bf --- /dev/null +++ b/src/qml/ConsoleCategoryRail.qml @@ -0,0 +1,161 @@ +// 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.mauikit.controls as Maui +import org.kde.alakarte + +Item { + id: root + + property string currentCategory: "all" + + signal categorySelected(string categoryId) + + function selectNext() { + if (rail.currentIndex < categoryModel.count - 1) { + rail.currentIndex++ + categorySelected(categoryModel.get(rail.currentIndex).categoryId) + } + } + + function selectPrevious() { + if (rail.currentIndex > 0) { + rail.currentIndex-- + categorySelected(categoryModel.get(rail.currentIndex).categoryId) + } + } + + function _platformIcon(name) { + let p = (name || "").toLowerCase() + if (p.includes("steam")) return ":/icons/brand/steam-symbolic.svg" + if (p.includes("itch")) return ":/icons/brand/itchdotio-symbolic.svg" + if (p.includes("retroarch")) return ":/icons/brand/retroarch-symbolic.svg" + if (p.includes("lutris")) return "applications-games" + if (p.includes("heroic")) return "applications-games" + if (p.includes("bottles")) return "application-x-executable" + if (p.includes("flatpak")) return "applications-games" + if (p.includes("desktop")) return "computer" + return "applications-games" + } + + GameSortFilterModel { + id: allGames + sourceModel: App.gameModel + showHidden: false + } + + ListModel { + id: categoryModel + + Component.onCompleted: rebuild() + + function rebuild() { + clear() + append({ label: i18n("All"), categoryId: "all", iconSource: "view-list-icons" }) + append({ label: i18n("Favorites"), categoryId: "favorites", iconSource: "starred-symbolic" }) + + let seen = {} + for (let i = 0; i < allGames.count; ++i) { + let g = allGames.get(i) + if (!g) continue + let p = g.platform || "" + if (p && !seen[p]) { + seen[p] = true + append({ label: p, categoryId: p, iconSource: root._platformIcon(p) }) + } + } + + for (let j = 0; j < count; ++j) { + if (get(j).categoryId === root.currentCategory) { + rail.currentIndex = j + break + } + } + } + } + + Connections { + target: App.gameModel + function onCountChanged() { categoryModel.rebuild() } + } + + onCurrentCategoryChanged: { + for (let j = 0; j < categoryModel.count; ++j) { + if (categoryModel.get(j).categoryId === currentCategory) { + rail.currentIndex = j + return + } + } + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.55) + + ListView { + id: rail + anchors { + fill: parent + leftMargin: 24 + rightMargin: 24 + } + orientation: ListView.Horizontal + spacing: 8 + clip: true + model: categoryModel + keyNavigationEnabled: true + highlightMoveDuration: 200 + + delegate: QQC2.ItemDelegate { + id: pill + width: implicitContentWidth + 32 + height: rail.height + topPadding: 0 + bottomPadding: 0 + leftPadding: 16 + rightPadding: 16 + + readonly property bool isCurrent: ListView.isCurrentItem + + background: Rectangle { + radius: height / 2 + color: pill.isCurrent + ? Maui.Theme.highlightColor + : (pill.hovered ? Qt.rgba(1, 1, 1, 0.12) : Qt.rgba(1, 1, 1, 0.06)) + border.color: pill.isCurrent ? Maui.Theme.highlightColor : Qt.rgba(1, 1, 1, 0.18) + border.width: 1 + + Behavior on color { ColorAnimation { duration: 120 } } + } + + contentItem: RowLayout { + spacing: 6 + + Maui.Icon { + source: model.iconSource + Layout.preferredWidth: 16 + Layout.preferredHeight: 16 + color: "white" + opacity: pill.isCurrent ? 1.0 : 0.75 + } + + QQC2.Label { + text: model.label + color: "white" + opacity: pill.isCurrent ? 1.0 : 0.75 + font.pixelSize: 14 + font.weight: pill.isCurrent ? Font.DemiBold : Font.Normal + } + } + + onClicked: { + rail.currentIndex = index + root.categorySelected(model.categoryId) + } + } + } + } +} diff --git a/src/qml/ConsoleGameDetail.qml b/src/qml/ConsoleGameDetail.qml new file mode 100644 index 0000000..de92137 --- /dev/null +++ b/src/qml/ConsoleGameDetail.qml @@ -0,0 +1,379 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import QtQuick.Effects +import org.mauikit.controls as Maui +import org.kde.alakarte + +Item { + id: detailRoot + + property var game: null + + signal close() + signal launch() + signal editRequested() + signal removeRequested() + + visible: game !== null + focus: visible + + Keys.onEscapePressed: detailRoot.close() + + Connections { + target: GamepadManager + function onBackPressed() { + if (detailRoot.visible) detailRoot.close() + } + function onSelectPressed() { + if (detailRoot.visible) detailRoot.launch() + } + function onDetailsPressed() { + if (detailRoot.visible) detailRoot.editRequested() + } + } + + Rectangle { + anchors.fill: parent + color: "#CC000000" + } + + Image { + id: artBackdrop + anchors.fill: parent + source: detailRoot.game ? detailRoot.game.coverUrl : "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + visible: false + } + + MultiEffect { + source: artBackdrop + anchors.fill: artBackdrop + blurEnabled: true + blur: 1.0 + blurMax: 80 + brightness: -0.2 + opacity: 0.45 + } + + Rectangle { + anchors.fill: parent + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0.82) } + GradientStop { position: 0.55; color: Qt.rgba(0, 0, 0, 0.46) } + GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.10) } + } + } + + Rectangle { + anchors.fill: parent + gradient: Gradient { + orientation: Gradient.Vertical + GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0.0) } + GradientStop { position: 0.72; color: Qt.rgba(0, 0, 0, 0.0) } + GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.75) } + } + } + + RowLayout { + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + top: parent.top + leftMargin: 64 + rightMargin: 48 + bottomMargin: 64 + topMargin: 48 + } + spacing: 48 + + Item { + Layout.preferredWidth: Math.round(parent.height * 0.42) + Layout.fillHeight: true + + Rectangle { + id: coverArt + anchors.centerIn: parent + width: Math.round(parent.width * 0.85) + height: Math.round(width * 1.45) + radius: 12 + color: "#1a1a2a" + clip: true + + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowColor: Qt.rgba(0, 0, 0, 0.7) + shadowBlur: 0.7 + shadowHorizontalOffset: 0 + shadowVerticalOffset: 12 + } + + Image { + anchors.fill: parent + source: detailRoot.game ? detailRoot.game.coverUrl : "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + smooth: true + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 0 + + Item { Layout.fillHeight: true; Layout.preferredHeight: 1 } + + QQC2.Label { + Layout.fillWidth: true + text: detailRoot.game ? detailRoot.game.name : "" + color: "white" + font.pixelSize: 52 + font.weight: Font.Bold + wrapMode: Text.WordWrap + maximumLineCount: 3 + elide: Text.ElideRight + lineHeight: 1.1 + style: Text.Raised + styleColor: Qt.rgba(0, 0, 0, 0.5) + } + + Item { Layout.preferredHeight: 12 } + + RowLayout { + spacing: 12 + visible: detailRoot.game && detailRoot.game.platform + + Rectangle { + radius: 4 + color: Qt.rgba(1, 1, 1, 0.15) + border.color: Qt.rgba(1, 1, 1, 0.3) + border.width: 1 + implicitWidth: platformLabel.implicitWidth + 20 + implicitHeight: platformLabel.implicitHeight + 8 + + QQC2.Label { + id: platformLabel + anchors.centerIn: parent + text: detailRoot.game ? (detailRoot.game.platform || "") : "" + color: "white" + font.pixelSize: 13 + font.weight: Font.Medium + } + } + + Rectangle { + radius: 4 + color: "#4caf50" + implicitWidth: runningLabel.implicitWidth + 16 + implicitHeight: runningLabel.implicitHeight + 6 + visible: detailRoot.game && detailRoot.game.running + + QQC2.Label { + id: runningLabel + anchors.centerIn: parent + text: i18n("Running") + color: "white" + font.pixelSize: 12 + font.weight: Font.Medium + } + } + } + + Item { Layout.preferredHeight: 48 } + + RowLayout { + spacing: 16 + + QQC2.Button { + id: playBtn + text: i18n("Play") + icon.name: "media-playback-start" + focus: true + KeyNavigation.right: favoriteBtn + + contentItem: RowLayout { + spacing: 8 + Maui.Icon { + source: "media-playback-start" + width: 22; height: 22 + color: "white" + } + QQC2.Label { + text: i18n("Play") + color: "white" + font.pixelSize: 17 + font.weight: Font.DemiBold + } + } + + background: Rectangle { + radius: 8 + color: playBtn.activeFocus + ? Maui.Theme.highlightColor + : (playBtn.hovered ? Qt.rgba(1,1,1,0.22) : Qt.rgba(1,1,1,0.14)) + border.color: playBtn.activeFocus ? Maui.Theme.highlightColor : Qt.rgba(1,1,1,0.28) + border.width: playBtn.activeFocus ? 0 : 1 + Behavior on color { ColorAnimation { duration: 100 } } + } + + implicitWidth: 160 + implicitHeight: 52 + + onClicked: detailRoot.launch() + Keys.onReturnPressed: detailRoot.launch() + Keys.onEnterPressed: detailRoot.launch() + } + + QQC2.Button { + id: favoriteBtn + icon.name: detailRoot.game && detailRoot.game.favorite ? "starred-symbolic" : "non-starred-symbolic" + KeyNavigation.left: playBtn + KeyNavigation.right: editBtn + + contentItem: Maui.Icon { + source: detailRoot.game && detailRoot.game.favorite ? "starred-symbolic" : "non-starred-symbolic" + width: 22; height: 22 + color: detailRoot.game && detailRoot.game.favorite ? "#f5c518" : "white" + } + + background: Rectangle { + radius: 8 + color: favoriteBtn.activeFocus + ? Qt.rgba(1,1,1,0.22) + : (favoriteBtn.hovered ? Qt.rgba(1,1,1,0.18) : Qt.rgba(1,1,1,0.10)) + border.color: favoriteBtn.activeFocus ? "white" : Qt.rgba(1,1,1,0.25) + border.width: 1 + Behavior on color { ColorAnimation { duration: 100 } } + } + + implicitWidth: 52 + implicitHeight: 52 + QQC2.ToolTip.visible: hovered || activeFocus + QQC2.ToolTip.text: detailRoot.game && detailRoot.game.favorite + ? i18n("Remove from Favorites") : i18n("Add to Favorites") + QQC2.ToolTip.delay: 800 + + onClicked: if (detailRoot.game) detailRoot.game.favorite = !detailRoot.game.favorite + } + + QQC2.Button { + id: editBtn + icon.name: "document-edit" + KeyNavigation.left: favoriteBtn + KeyNavigation.right: closeBtn + + contentItem: Maui.Icon { + source: "document-edit" + width: 20; height: 20 + color: "white" + } + + background: Rectangle { + radius: 8 + color: editBtn.activeFocus + ? Qt.rgba(1,1,1,0.22) + : (editBtn.hovered ? Qt.rgba(1,1,1,0.18) : Qt.rgba(1,1,1,0.10)) + border.color: editBtn.activeFocus ? "white" : Qt.rgba(1,1,1,0.25) + border.width: 1 + Behavior on color { ColorAnimation { duration: 100 } } + } + + implicitWidth: 52 + implicitHeight: 52 + QQC2.ToolTip.visible: hovered || activeFocus + QQC2.ToolTip.text: i18n("Edit") + QQC2.ToolTip.delay: 800 + + onClicked: detailRoot.editRequested() + } + + QQC2.Button { + id: closeBtn + icon.name: "window-close" + KeyNavigation.left: editBtn + + contentItem: Maui.Icon { + source: "window-close" + width: 20; height: 20 + color: "white" + } + + background: Rectangle { + radius: 8 + color: closeBtn.activeFocus + ? Qt.rgba(1,1,1,0.22) + : (closeBtn.hovered ? Qt.rgba(1,1,1,0.18) : Qt.rgba(1,1,1,0.10)) + border.color: closeBtn.activeFocus ? "white" : Qt.rgba(1,1,1,0.25) + border.width: 1 + Behavior on color { ColorAnimation { duration: 100 } } + } + + implicitWidth: 52 + implicitHeight: 52 + QQC2.ToolTip.visible: hovered || activeFocus + QQC2.ToolTip.text: i18n("Back") + QQC2.ToolTip.delay: 800 + + onClicked: detailRoot.close() + } + } + + Item { Layout.preferredHeight: 32 } + + RowLayout { + spacing: 28 + + Repeater { + model: [ + { icon: ":/icons/gamepad/generic/south.svg", label: i18n("Play") }, + { icon: ":/icons/gamepad/generic/east.svg", label: i18n("Back") }, + { icon: ":/icons/gamepad/generic/north.svg", label: i18n("Edit") } + ] + + RowLayout { + spacing: 6 + visible: GamepadManager.connected + + Image { + source: modelData.icon + width: 22; height: 22 + fillMode: Image.PreserveAspectFit + smooth: true + } + + QQC2.Label { + text: modelData.label + color: Qt.rgba(1, 1, 1, 0.65) + font.pixelSize: 13 + } + } + } + } + + Item { Layout.fillHeight: true; Layout.preferredHeight: 1 } + } + } + + NumberAnimation on opacity { + id: fadeIn + running: detailRoot.visible + from: 0; to: 1 + duration: 220 + easing.type: Easing.OutCubic + } + + onVisibleChanged: { + if (visible) { + Qt.callLater(function() { playBtn.forceActiveFocus() }) + } + } +} diff --git a/src/qml/GameCard.qml b/src/qml/GameCard.qml index a139cc6..0dd5d43 100644 --- a/src/qml/GameCard.qml +++ b/src/qml/GameCard.qml @@ -1,314 +1,168 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// SPDX-FileCopyrightText: 2024 A-La-Karte Contributors +// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors import QtQuick import QtQuick.Controls as QQC2 import QtQuick.Layouts import QtQuick.Effects -import org.kde.kirigami as Kirigami +import org.mauikit.controls as Maui import org.kde.alakarte -FocusScope { +Item { id: gameCard property var game - property bool showPlayButton: true - property bool focused: activeFocus + signal clicked() + signal playClicked() - readonly property bool isTouchDevice: { - let w = applicationWindow() - if (w && w.isTouchDevice !== undefined) return w.isTouchDevice - return Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile - } - readonly property real adaptiveHoverScale: root.isCouchMode ? 1.05 : 1.015 - readonly property real adaptiveFocusScale: root.isCouchMode ? 1.12 : 1.03 - readonly property int adaptiveFocusRingWidth: root.isCouchMode ? 3 : 1 + readonly property bool isCurrent: GridView.isCurrentItem readonly property bool useAnimatedCover: App.config.animatedCovers - && game - && game.coverUrl + && game && game.coverUrl && game.coverUrl.toString().toLowerCase().endsWith(".gif") readonly property int coverStatus: useAnimatedCover ? animatedCover.status : staticCover.status - signal clicked() - signal doubleClicked() - signal playClicked() + width: GridView.view ? GridView.view.cellWidth : 200 + height: GridView.view ? GridView.view.cellHeight : 300 - Kirigami.ShadowedRectangle { - id: cardBackground - anchors.fill: parent - radius: Kirigami.Units.mediumSpacing + Item { + id: cardContainer + anchors.centerIn: parent + width: parent.width - 16 + height: parent.height - 16 - color: Kirigami.Theme.backgroundColor - - shadow { - size: gameCard.focused ? (root.isCouchMode ? Kirigami.Units.gridUnit * 1.5 : Kirigami.Units.mediumSpacing) : (hoverHandler.hovered ? Kirigami.Units.smallSpacing * 1.5 : Kirigami.Units.smallSpacing) - color: gameCard.focused ? (root.isCouchMode ? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.4) : Qt.rgba(0, 0, 0, 0.34)) : (hoverHandler.hovered ? Qt.rgba(0, 0, 0, 0.24) : Qt.rgba(0, 0, 0, 0.16)) - } - - Behavior on shadow.color { - ColorAnimation { duration: Kirigami.Units.shortDuration } - } - - border.width: gameCard.focused ? gameCard.adaptiveFocusRingWidth : 0 - border.color: Kirigami.Theme.highlightColor - - Behavior on border.width { - NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } - } - - Behavior on shadow.size { - NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } - } - - y: gameCard.focused ? -Kirigami.Units.smallSpacing : (hoverHandler.hovered ? -Kirigami.Units.smallSpacing * 0.5 : 0) - Behavior on y { - NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } - } - - scale: gameCard.focused ? gameCard.adaptiveFocusScale : (hoverHandler.hovered ? gameCard.adaptiveHoverScale : 1.0) + scale: gameCard.isCurrent ? 1.08 : (hoverHandler.hovered ? 1.03 : 1.0) Behavior on scale { - NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + NumberAnimation { duration: 140; easing.type: Easing.OutCubic } + } + + layer.enabled: gameCard.isCurrent + layer.effect: MultiEffect { + shadowEnabled: true + shadowColor: Qt.rgba( + Maui.Theme.highlightColor.r, + Maui.Theme.highlightColor.g, + Maui.Theme.highlightColor.b, 0.55) + shadowBlur: 0.8 + shadowHorizontalOffset: 0 + shadowVerticalOffset: 6 } Rectangle { + id: coverFrame anchors.fill: parent - radius: Kirigami.Units.mediumSpacing - color: "transparent" - border.width: 2 - border.color: Kirigami.Theme.highlightColor - opacity: 0.0 - visible: gameCard.focused + radius: 8 + color: "#1a1a2a" + clip: true - SequentialAnimation on opacity { - running: gameCard.focused - loops: Animation.Infinite - NumberAnimation { from: 0.10; to: 0.28; duration: 900; easing.type: Easing.InOutQuad } - NumberAnimation { from: 0.28; to: 0.12; duration: 900; easing.type: Easing.InOutQuad } + Image { + id: staticCover + anchors.fill: parent + source: game ? game.coverUrl : "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + visible: !gameCard.useAnimatedCover + smooth: true + mipmap: true } - } - // Cover image - Image { - id: staticCover - anchors.fill: parent - anchors.margins: Kirigami.Units.smallSpacing - source: game ? game.coverUrl : "" - fillMode: Image.PreserveAspectCrop - asynchronous: true - visible: !gameCard.useAnimatedCover - smooth: true - mipmap: App.config.highQualityImages - sourceSize.width: Math.round(width * (App.config.highQualityImages ? 2 : 1)) - sourceSize.height: Math.round(height * (App.config.highQualityImages ? 2 : 1)) - - layer.enabled: true - layer.effect: MultiEffect { - maskEnabled: true - maskSource: ShaderEffectSource { - sourceItem: Rectangle { - width: staticCover.width - height: staticCover.height - radius: Kirigami.Units.mediumSpacing - Kirigami.Units.smallSpacing - } - } + AnimatedImage { + id: animatedCover + anchors.fill: parent + source: game ? game.coverUrl : "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + playing: gameCard.isCurrent + visible: gameCard.useAnimatedCover + smooth: true } - } - AnimatedImage { - id: animatedCover - anchors.fill: parent - anchors.margins: Kirigami.Units.smallSpacing - source: game ? game.coverUrl : "" - fillMode: Image.PreserveAspectCrop - asynchronous: true - playing: true - visible: gameCard.useAnimatedCover - smooth: true - mipmap: App.config.highQualityImages - sourceSize.width: Math.round(width * (App.config.highQualityImages ? 2 : 1)) - sourceSize.height: Math.round(height * (App.config.highQualityImages ? 2 : 1)) - - layer.enabled: true - layer.effect: MultiEffect { - maskEnabled: true - maskSource: ShaderEffectSource { - sourceItem: Rectangle { - width: animatedCover.width - height: animatedCover.height - radius: Kirigami.Units.mediumSpacing - Kirigami.Units.smallSpacing - } - } - } - } - - // Placeholder when no cover - Rectangle { - anchors.fill: parent - anchors.margins: Kirigami.Units.smallSpacing - radius: Kirigami.Units.mediumSpacing - Kirigami.Units.smallSpacing - color: Kirigami.Theme.alternateBackgroundColor - visible: gameCard.coverStatus !== Image.Ready - - Kirigami.Icon { + Maui.Icon { anchors.centerIn: parent source: "applications-games" - width: parent.width * 0.4 + width: parent.width * 0.38 height: width - color: Kirigami.Theme.disabledTextColor + color: "#44ffffff" + visible: gameCard.coverStatus !== Image.Ready } - } - // Gradient overlay for text - Rectangle { - opacity: (!root.isCouchMode || gameCard.focused || hoverHandler.hovered) ? 1.0 : 0.0 - Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } } - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: Kirigami.Units.smallSpacing - height: parent.height * 0.4 - radius: Kirigami.Units.mediumSpacing - Kirigami.Units.smallSpacing - - gradient: Gradient { - GradientStop { position: 0.0; color: "transparent" } - GradientStop { position: 0.5; color: Qt.rgba(0, 0, 0, 0.5) } - GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.85) } + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: parent.height * 0.42 + gradient: Gradient { + GradientStop { position: 0.0; color: "transparent" } + GradientStop { position: 0.55; color: Qt.rgba(0, 0, 0, 0.62) } + GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.92) } + } } - } - - // Game title - ColumnLayout { - opacity: (!root.isCouchMode || gameCard.focused || hoverHandler.hovered) ? 1.0 : 0.0 - Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } } - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: Kirigami.Units.mediumSpacing - spacing: Kirigami.Units.smallSpacing QQC2.Label { - Layout.fillWidth: true + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + margins: 10 + } text: game ? game.name : "" - font.bold: true - font.pointSize: Kirigami.Theme.defaultFont.pointSize color: "white" + font.pixelSize: 13 + font.weight: Font.Medium elide: Text.ElideRight wrapMode: Text.WordWrap maximumLineCount: 2 + opacity: gameCard.isCurrent || hoverHandler.hovered ? 1.0 : 0.85 } - RowLayout { - Layout.fillWidth: true - spacing: Kirigami.Units.smallSpacing - visible: App.config.showPlatformBadges - - Rectangle { - implicitWidth: platformLabel.implicitWidth + Kirigami.Units.largeSpacing - implicitHeight: platformLabel.implicitHeight + Kirigami.Units.smallSpacing - Layout.preferredWidth: implicitWidth - Layout.preferredHeight: implicitHeight - radius: Kirigami.Units.smallSpacing - color: getPlatformColor(game ? game.platform : "") - - QQC2.Label { - id: platformLabel - anchors.centerIn: parent - text: getPlatformDisplayName(game ? game.platform : "") - font.pointSize: Kirigami.Theme.smallFont.pointSize - color: "white" - } - } - - Item { Layout.fillWidth: true } - } - } - - // Favorite indicator - Kirigami.Icon { - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: Kirigami.Units.mediumSpacing - width: Kirigami.Units.iconSizes.medium - height: width - source: "bookmark-new" - color: Kirigami.Theme.positiveTextColor - visible: game && game.favorite - - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowColor: Qt.rgba(0, 0, 0, 0.5) - shadowBlur: 0.5 - } - } - - // Running indicator - Rectangle { - anchors.top: parent.top - anchors.left: parent.left - anchors.margins: Kirigami.Units.mediumSpacing - width: Kirigami.Units.iconSizes.small - height: width - radius: width / 2 - color: Kirigami.Theme.positiveTextColor - visible: game && game.running - - SequentialAnimation on opacity { - running: game && game.running - loops: Animation.Infinite - NumberAnimation { to: 0.3; duration: 800 } - NumberAnimation { to: 1.0; duration: 800 } - } - } - - // Play button overlay - Rectangle { - id: playOverlay - anchors.fill: parent - anchors.margins: Kirigami.Units.smallSpacing - radius: Kirigami.Units.mediumSpacing - Kirigami.Units.smallSpacing - color: Qt.rgba(0, 0, 0, 0.6) - opacity: (hoverHandler.hovered || gameCard.focused) && showPlayButton ? 1 : 0 - visible: opacity > 0 - - Behavior on opacity { - NumberAnimation { duration: Kirigami.Units.shortDuration } - } - - Kirigami.Icon { - anchors.centerIn: parent - width: Kirigami.Units.iconSizes.huge - height: width - source: "media-playback-start" - color: "white" - } - } - - HoverHandler { - id: hoverHandler - } - - TapHandler { - onTapped: { - if (App.config.coverLaunchesGame) { - gameCard.playClicked() - } else { - gameCard.clicked() + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: 8 + width: 9 + height: 9 + radius: 5 + color: "#4caf50" + visible: game && game.running + SequentialAnimation on opacity { + running: game && game.running + loops: Animation.Infinite + NumberAnimation { to: 0.25; duration: 700 } + NumberAnimation { to: 1.0; duration: 700 } } } - } - TapHandler { - acceptedButtons: Qt.LeftButton - onDoubleTapped: { - gameCard.doubleClicked() - gameCard.playClicked() + Maui.Icon { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: 8 + width: 18 + height: 18 + source: "starred-symbolic" + color: "#f5c518" + visible: game && game.favorite } } - // Context menu + Rectangle { + anchors.fill: coverFrame + radius: 8 + color: "transparent" + border.color: Maui.Theme.highlightColor + border.width: gameCard.isCurrent ? 3 : 0 + opacity: gameCard.isCurrent ? 1.0 : 0.0 + Behavior on opacity { NumberAnimation { duration: 120 } } + Behavior on border.width { NumberAnimation { duration: 120 } } + } + + HoverHandler { id: hoverHandler } + + TapHandler { + onTapped: gameCard.clicked() + onDoubleTapped: gameCard.playClicked() + } + TapHandler { acceptedButtons: Qt.RightButton onTapped: contextMenu.popup() @@ -322,48 +176,23 @@ FocusScope { icon.name: "media-playback-start" onTriggered: gameCard.playClicked() } - QQC2.MenuSeparator {} - QQC2.MenuItem { text: game && game.favorite ? i18n("Remove from Favorites") : i18n("Add to Favorites") - icon.name: game && game.favorite ? "bookmark-remove" : "bookmark-new" + icon.name: game && game.favorite ? "bookmark-remove" : "starred-symbolic" onTriggered: if (game) game.favorite = !game.favorite } - QQC2.MenuItem { text: game && game.hidden ? i18n("Show in Library") : i18n("Hide from Library") icon.name: game && game.hidden ? "view-visible" : "view-hidden" onTriggered: if (game) game.hidden = !game.hidden } - QQC2.MenuSeparator {} - QQC2.MenuItem { text: i18n("View Details") - icon.name: "documentation" + icon.name: "documentinfo" onTriggered: gameCard.clicked() } } } - - function getPlatformColor(platform) { - if (!platform) return Kirigami.Theme.highlightColor - if (platform.includes("Steam")) return "#1b2838" - if (platform.includes("Lutris")) return "#ff9800" - if (platform.includes("Epic")) return "#0078f2" - if (platform.includes("GOG")) return "#86328a" - if (platform.includes("Amazon")) return "#ff9900" - return Kirigami.Theme.highlightColor - } - - function getPlatformDisplayName(platform) { - if (!platform) return "" - if (platform.includes("Steam")) return "Steam" - if (platform.includes("Lutris")) return "Lutris" - if (platform.includes("Epic")) return "Epic" - if (platform.includes("GOG")) return "GOG" - if (platform.includes("Amazon")) return "Amazon" - return platform - } } diff --git a/src/qml/LibraryView.qml b/src/qml/LibraryView.qml index 1473da7..4f2df30 100644 --- a/src/qml/LibraryView.qml +++ b/src/qml/LibraryView.qml @@ -1,11 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// SPDX-FileCopyrightText: 2024 A-La-Karte Contributors +// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors import QtQuick import QtQuick.Controls as QQC2 import QtQuick.Layouts -import QtQuick.Effects -import org.kde.kirigami as Kirigami +import org.mauikit.controls as Maui import org.kde.alakarte import "components" @@ -13,321 +12,125 @@ FocusScope { id: libraryRoot property string filterSource: "all" - property bool searchActive: false - property int focusedIndex: -1 - - property int adaptiveCardSize: App.config.gridSize - property bool isTouchDevice: false + property string filterText: "" signal gameSelected(var game) + signal gameFocused(var game) signal gameLaunched(var game) readonly property int gameCount: proxyModel.count - property url focusedCoverUrl: "" - - readonly property bool anyMenuOpen: searchHeader.anyMenuOpen - - function closeCurrentMenu() { - searchHeader.closeCurrentMenu() - } - - function focusSearch() { - searchField.forceActiveFocus() - } - - function clearSearch() { - searchField.text = "" - proxyModel.filterText = "" - } function restoreFocus() { - let w = applicationWindow() - if (w && w.hasOwnProperty("pendingSidebarOpen") && w.pendingSidebarOpen) { - w.pendingSidebarOpen = false - if (w.globalDrawer && typeof w.globalDrawer.open === "function") { - w.globalDrawer.open() - return - } - } - if (libraryRoot.searchActive) { - libraryRoot.focusSearch() - } else { - if (libraryRoot.focusedIndex >= 0 && libraryRoot.focusedIndex < proxyModel.count) { - gameGrid.currentIndex = libraryRoot.focusedIndex - } - gameGrid.forceActiveFocus() - } + grid.forceActiveFocus() } - function launchFocusedGame() { - if (!gameGrid || !proxyModel) return - if (gameGrid.currentIndex < 0 && proxyModel.count > 0) { - gameGrid.currentIndex = 0 - libraryRoot.focusedIndex = 0 - } - let game = proxyModel.get(gameGrid.currentIndex) - if (game) { - libraryRoot.gameLaunched(game) - } + function focusedGame() { + if (grid.currentIndex < 0) return null + return proxyModel.get(grid.currentIndex) } - function openDetailsForFocusedGame() { - if (!gameGrid || !proxyModel) return - if (gameGrid.currentIndex < 0 && proxyModel.count > 0) { - gameGrid.currentIndex = 0 - libraryRoot.focusedIndex = 0 - } - let game = proxyModel.get(gameGrid.currentIndex) - if (game) { - libraryRoot.gameSelected(game) - } + function selectFocused() { + let g = focusedGame() + if (g) libraryRoot.gameSelected(g) } - onSearchActiveChanged: { - if (!libraryRoot.searchActive) { - libraryRoot.clearSearch() - Qt.callLater(function() { - gameGrid.forceActiveFocus() - }) - } + function launchFocused() { + let g = focusedGame() + if (g) libraryRoot.gameLaunched(g) } - Item { + GameSortFilterModel { + id: proxyModel + sourceModel: App.gameModel + showHidden: false + favoritesOnly: libraryRoot.filterSource === "favorites" + filterSource: { + if (libraryRoot.filterSource === "all") return "" + if (libraryRoot.filterSource === "favorites") return "" + return libraryRoot.filterSource + } + filterText: libraryRoot.filterText + } + + Maui.GridBrowser { + id: grid anchors.fill: parent - anchors.margins: 0 + focus: true - Item { - anchors.fill: parent - visible: libraryRoot.gameCount > 0 + model: proxyModel + itemSize: Math.max(160, Math.min(App.config.gridSize, 280)) + itemHeight: Math.round(grid.itemSize * 1.5) + adaptContent: true + enableLassoSelection: false - Image { - id: backgroundCoverA - anchors.fill: parent - source: "" - fillMode: Image.PreserveAspectCrop - asynchronous: true - visible: source.toString().length > 0 - smooth: true - mipmap: App.config.highQualityImages - opacity: 0.0 + topPadding: 16 + bottomPadding: 24 + leftPadding: 24 + rightPadding: 24 - Behavior on opacity { - NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic } - } + holder.visible: proxyModel.count === 0 && !App.importing + holder.emoji: libraryRoot.filterSource === "favorites" ? "starred-symbolic" + : "applications-games" + holder.title: libraryRoot.filterSource === "favorites" + ? i18n("No favorites yet") + : i18n("Your library is empty") + holder.body: libraryRoot.filterSource === "favorites" + ? i18n("Mark games as favorites to see them here") + : i18n("Import games to get started") - layer.enabled: true - layer.effect: MultiEffect { - blurEnabled: true - blur: 0.9 - blurMax: 64 - } + onCurrentIndexChanged: { + let g = proxyModel.get(currentIndex) + if (g) libraryRoot.gameFocused(g) + } + + delegate: GameCard { + game: model.gameObject + + onClicked: { + grid.currentIndex = index + libraryRoot.gameSelected(model.gameObject) } - - Image { - id: backgroundCoverB - anchors.fill: parent - source: "" - fillMode: Image.PreserveAspectCrop - asynchronous: true - visible: source.toString().length > 0 - smooth: true - mipmap: App.config.highQualityImages - opacity: 0.0 - - Behavior on opacity { - NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic } - } - - layer.enabled: true - layer.effect: MultiEffect { - blurEnabled: true - blur: 0.9 - blurMax: 64 - } - } - - Rectangle { - anchors.fill: parent - color: Qt.rgba(0, 0, 0, 0.55) + onPlayClicked: { + grid.currentIndex = index + libraryRoot.gameLaunched(model.gameObject) } } - ColumnLayout { - anchors.fill: parent - spacing: Kirigami.Units.smallSpacing + Keys.onReturnPressed: libraryRoot.selectFocused() + Keys.onEnterPressed: libraryRoot.selectFocused() + Keys.onSpacePressed: libraryRoot.selectFocused() - SearchHeader { - id: searchHeader - Layout.fillWidth: true - visible: libraryRoot.searchActive - - // In Couch Mode, position it absolute over the grid instead of shifting the grid - // to mimic a console drop-down search bar - y: root.isCouchMode ? Kirigami.Units.gridUnit * 2 : 0 - z: 100 - - searchField: searchField - - onSearchChanged: function(text) { - proxyModel.filterText = text - } - - onSortChanged: function(mode) { - proxyModel.sortMode = mode - } - - Kirigami.SearchField { - id: searchField - Layout.fillWidth: true - placeholderText: i18n("Search games...") - - onTextChanged: proxyModel.filterText = text - - Keys.onEscapePressed: { - text = "" - let w = applicationWindow() - if (w && w.hasOwnProperty("searchActive")) { - w.searchActive = false - } else { - libraryRoot.searchActive = false - } - libraryRoot.restoreFocus() - } - - Keys.onDownPressed: gameGrid.forceActiveFocus() - - Connections { - target: GamepadManager - function onNavigateDown() { - let w = applicationWindow() - if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return - if (!searchField.activeFocus) return - gameGrid.forceActiveFocus() - } - } - } + QQC2.BusyIndicator { + anchors.centerIn: parent + running: App.importing + visible: App.importing } - GameGridView { - id: gameGrid - Layout.fillWidth: true - Layout.fillHeight: true - - cardSize: libraryRoot.adaptiveCardSize - - onCurrentIndexChanged: { - if (gameGrid.activeFocus) { - libraryRoot.focusedIndex = currentIndex - } - let game = proxyModel.get(currentIndex) - let url = (game && game.coverUrl) ? game.coverUrl : "" - if (url === libraryRoot.focusedCoverUrl) return - if (backgroundCoverA.opacity > 0.1) { - backgroundCoverB.source = url - backgroundCoverB.opacity = 0.0 - backgroundCoverA.opacity = 0.0 - Qt.callLater(function() { backgroundCoverB.opacity = 0.22 }) - } else { - backgroundCoverA.source = url - backgroundCoverA.opacity = 0.0 - backgroundCoverB.opacity = 0.0 - Qt.callLater(function() { backgroundCoverA.opacity = 0.22 }) - } - libraryRoot.focusedCoverUrl = url + Connections { + target: GamepadManager + function onSelectPressed() { + if (!grid.activeFocus) return + libraryRoot.selectFocused() } - - model: GameSortFilterModel { - id: proxyModel - sourceModel: App.gameModel - showHidden: libraryRoot.filterSource === "hidden" - favoritesOnly: libraryRoot.filterSource === "favorites" - filterSource: { - if (libraryRoot.filterSource === "all") return "" - if (libraryRoot.filterSource === "favorites") return "" - if (libraryRoot.filterSource === "hidden") return "" - return libraryRoot.filterSource - } - } - - delegate: Item { - width: gameGrid.cellWidth - height: gameGrid.cellHeight - - function clicked() { - gameGrid.currentIndex = index - libraryRoot.focusedIndex = index - card.clicked() - } - - function play() { - gameGrid.currentIndex = index - libraryRoot.focusedIndex = index - card.playClicked() - } - - GameCard { - id: card - width: gameGrid.cardSize - height: Math.round(gameGrid.cardSize * 1.4) - anchors.centerIn: parent - - game: model.gameObject - focused: gameGrid.currentIndex === index && gameGrid.activeFocus - - onClicked: libraryRoot.gameSelected(model.gameObject) - onDoubleClicked: libraryRoot.gameLaunched(model.gameObject) - onPlayClicked: libraryRoot.gameLaunched(model.gameObject) - - Keys.onReturnPressed: libraryRoot.gameSelected(model.gameObject) - Keys.onEnterPressed: libraryRoot.gameSelected(model.gameObject) - Keys.onSpacePressed: libraryRoot.gameLaunched(model.gameObject) - } + function onNavigateUp() { + if (!grid.activeFocus) return + if (grid.currentIndex <= 0) return + let cols = Math.max(1, Math.floor(grid.width / grid.cellWidth)) + grid.currentIndex = Math.max(0, grid.currentIndex - cols) } - - Keys.onPressed: function(event) { - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - if (currentIndex < 0 && proxyModel.count > 0) { - currentIndex = 0 - libraryRoot.focusedIndex = 0 - } - let game = proxyModel.get(currentIndex) - if (game) { - libraryRoot.gameSelected(game) - } - event.accepted = true - } else if (event.key === Qt.Key_Space) { - if (currentIndex < 0 && proxyModel.count > 0) { - currentIndex = 0 - libraryRoot.focusedIndex = 0 - } - let game = proxyModel.get(currentIndex) - if (game) { - libraryRoot.gameLaunched(game) - } - event.accepted = true - } + function onNavigateDown() { + if (!grid.activeFocus) return + let cols = Math.max(1, Math.floor(grid.width / grid.cellWidth)) + grid.currentIndex = Math.min(proxyModel.count - 1, grid.currentIndex + cols) } - - EmptyState { - anchors.centerIn: parent - visible: proxyModel.count === 0 && !App.importing - - icon: proxyModel.filterText.length > 0 ? "edit-find" : (libraryRoot.filterSource === "favorites" ? "bookmark-new" : (libraryRoot.filterSource === "hidden" ? "view-hidden" : "applications-games")) - title: proxyModel.filterText.length > 0 ? - i18n("No games found") : (libraryRoot.filterSource === "favorites" ? i18n("No favorites yet") : (libraryRoot.filterSource === "hidden" ? i18n("No hidden games") : i18n("Your library is empty"))) - description: proxyModel.filterText.length > 0 ? - i18n("Try adjusting your search") : (libraryRoot.filterSource === "favorites" ? i18n("Mark games as favorites to see them here") : (libraryRoot.filterSource === "hidden" ? i18n("Hidden games will appear here") : i18n("Import games to get started"))) - - actionText: (proxyModel.filterText.length > 0 || libraryRoot.filterSource === "favorites" || libraryRoot.filterSource === "hidden") ? "" : i18n("Import Games") - onActionTriggered: App.importAllGames() + function onNavigateLeft() { + if (!grid.activeFocus) return + grid.currentIndex = Math.max(0, grid.currentIndex - 1) } - - QQC2.BusyIndicator { - anchors.centerIn: parent - running: App.importing - visible: App.importing + function onNavigateRight() { + if (!grid.activeFocus) return + grid.currentIndex = Math.min(proxyModel.count - 1, grid.currentIndex + 1) } } - } } } diff --git a/src/qml/Main.qml b/src/qml/Main.qml index 8274964..c262852 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -4,2130 +4,353 @@ import QtQuick import QtQuick.Controls as QQC2 import QtQuick.Layouts -import QtQuick.Window -import org.kde.kirigami as Kirigami -import org.kde.kirigamiaddons.formcard as FormCard +import QtQuick.Effects +import org.mauikit.controls as Maui import org.kde.alakarte -import "components" -Kirigami.ApplicationWindow { +Maui.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 bool windowGeometryRestored: false - - Timer { - id: windowGeometrySaveTimer - interval: 400 - repeat: false - onTriggered: { - if (!root.windowGeometryRestored) return - if (root.isMobile) return - if (root.visibility === Window.Maximized || root.visibility === Window.FullScreen) return - App.config.setWindowSize(root.width, root.height) - } - } - - onWidthChanged: { - if (!root.windowGeometryRestored) return - if (root.isMobile) return - windowGeometrySaveTimer.restart() - } - onHeightChanged: { - if (!root.windowGeometryRestored) return - if (root.isMobile) return - windowGeometrySaveTimer.restart() - } - - onVisibilityChanged: { - if (!root.windowGeometryRestored) return - if (root.isMobile) return - App.config.windowMaximized = root.visibility === Window.Maximized - } - - onClosing: function(close) { - if (root.isMobile) return - App.config.windowMaximized = root.visibility === Window.Maximized - if (root.visibility !== Window.Maximized && root.visibility !== Window.FullScreen) { - App.config.setWindowSize(root.width, root.height) - } - } + minimumWidth: 800 + minimumHeight: 500 + property var focusedGame: null property var selectedGame: null - property var pendingRemoveGame: null - property string currentSource: "all" - property bool searchActive: false - property bool settingsLayerOpen: false - property bool pendingSidebarOpen: false - property bool pendingAboutOpen: false + property string currentCategory: "all" + property bool detailVisible: false + property bool searchVisible: false + property bool settingsVisible: false + property bool importVisible: false + property string searchText: "" - readonly property string hintContext: { - if (gameEditDialog.visible) return "edit" - if (removeGameConfirmDialog.opened) return "confirm_remove" - { - let layerContent = root.settingsLayerContentItem() - if (settingsLayerOpen && layerContent && layerContent.anyConfirmOpen) return "confirm" - } - if (settingsSheet.opened && settingsContent.anyConfirmOpen) return "confirm" - if (diagnosticsSheet.opened) return "diagnostics" - if (detailsSheet.opened) return "details" - if (importSheet.opened) return "import" - if (settingsSheet.opened) return "settings" - if (settingsLayerOpen) return "settings" - if (aboutSheet.opened) return "about" - if (sidebar.opened && (sidebar.modal || sidebarView.hasSidebarFocus)) return "sidebar" - return "library" + color: "#0d0d14" + + Component.onCompleted: { + libraryView.restoreFocus() } - 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 - if (p.visualParent !== undefined && p.visualParent !== null) { - if (root.isDescendant(p.visualParent, ancestor)) return true - } else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) { - if (root.isDescendant(p.popup.visualParent, ancestor)) return true - } - p = p.parent - } - return false - } + Item { + id: backdrop + anchors.fill: parent + z: 0 - function currentConfirmDialog() { - if (removeGameConfirmDialog.opened) return removeGameConfirmDialog - if (gameEditDialog.visible && gameEditDialog.anyConfirmOpen) return gameEditDialog.currentConfirmDialog() - let layerContent = root.settingsLayerContentItem() - if (settingsLayerOpen && layerContent && layerContent.anyConfirmOpen) return layerContent.currentConfirmDialog() - if (settingsSheet.opened && settingsContent.anyConfirmOpen) return settingsContent.currentConfirmDialog() - return null - } - - function settingsLayerContentItem() { - if (!settingsLayerOpen) return null - if (!root.pageStack || !root.pageStack.layers) return null - let item = root.pageStack.layers.currentItem - if (!item) return null - if (item.content !== undefined && item.content !== null) return item.content - return null - } - - function focusNextInDialog(dialog, forward) { - let w = applicationWindow() - if (!w || !w.activeFocusItem || !dialog) return - - if (typeof dialog.standardButton === "function") { - let yesButton = dialog.standardButton(Kirigami.Dialog.Yes) - let noButton = dialog.standardButton(Kirigami.Dialog.No) - if (yesButton && noButton) { - if (w.activeFocusItem === yesButton) { - noButton.forceActiveFocus() - return - } - if (w.activeFocusItem === noButton) { - yesButton.forceActiveFocus() - return - } - (forward ? yesButton : noButton).forceActiveFocus() - return - } - } - - let next = w.activeFocusItem - for (let i = 0; i < 50; i++) { - next = next.nextItemInFocusChain(forward) - if (!next) return - if (root.isDescendant(next, dialog)) { - next.forceActiveFocus() - return - } - } - } - - function activateFocusedInDialog(dialog) { - let w = applicationWindow() - if (!w || !w.activeFocusItem || !dialog) return - - if (!root.isDescendant(w.activeFocusItem, dialog)) { - if (typeof dialog.standardButton === "function") { - let noButton = dialog.standardButton(Kirigami.Dialog.No) - if (noButton) { - noButton.forceActiveFocus() - return - } - } - dialog.forceActiveFocus() - return - } - - let item = w.activeFocusItem - if (typeof item.clicked === "function") { - item.clicked() - } - } - - function closeTopmost() { - if (libraryView && libraryView.anyMenuOpen) { - libraryView.closeCurrentMenu() - return true - } - if (gameEditDialog.visible) { - if (gameEditDialog.anyConfirmOpen) { - gameEditDialog.closeCurrentConfirmDialog() - return true - } - if (gameEditDialog.anyMenuOpen) { - gameEditDialog.closeCurrentMenu() - return true - } - gameEditDialog.close() - return true - } - if (removeGameConfirmDialog.opened) { - removeGameConfirmDialog.close() - root.pendingRemoveGame = null - return true - } - let layerContent = root.settingsLayerContentItem() - if (settingsLayerOpen && layerContent && layerContent.anyConfirmOpen) { - layerContent.closeCurrentConfirmDialog() - return true - } - if (settingsLayerOpen && layerContent && layerContent.anyMenuOpen) { - layerContent.closeCurrentMenu() - return true - } - if (settingsSheet.opened && settingsContent.anyConfirmOpen) { - settingsContent.closeCurrentConfirmDialog() - return true - } - if (settingsSheet.opened && settingsContent.anyMenuOpen) { - settingsContent.closeCurrentMenu() - return true - } - if (settingsSheet.opened) { - settingsSheet.close() - return true - } - if (diagnosticsSheet.opened) { - diagnosticsSheet.close() - return true - } - if (detailsSheet.opened) { - detailsSheet.close() - return true - } - if (importSheet.opened) { - importSheet.close() - return true - } - if (aboutSheet.opened) { - aboutSheet.close() - return true - } - if (sidebar.opened && (sidebar.modal || sidebarView.hasSidebarFocus)) { - if (sidebar.modal) { - sidebar.close() - } else { - Qt.callLater(function() { libraryView.restoreFocus() }) - } - return true - } - if (root.pageStack.layers.depth > 1) { - root.pageStack.layers.pop() - Qt.callLater(function() { libraryView.restoreFocus() }) - return true - } - if (searchActive) { - searchActive = false - libraryView.clearSearch() - Qt.callLater(function() { libraryView.restoreFocus() }) - return true - } - return false - } - - function canOpenSettings() { - return !gameEditDialog.visible - && !detailsSheet.opened - && !diagnosticsSheet.opened - && !importSheet.opened - && !aboutSheet.opened - && !settingsSheet.opened - && !(sidebar.modal && sidebar.opened) - && root.pageStack.layers.depth <= 1 - } - - 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 - } - - function uiModeLabel(mode) { - if (mode === Config.Desktop) return i18n("Desktop") - if (mode === Config.Couch) return i18n("Couch") - 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.Couch - } 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 int effectiveUiMode: { - if (App.config.uiMode === Config.Auto) { - if (GamepadManager.connected) { - if (InputManager.activeInput === InputManager.Gamepad) { - return Config.Couch - } - if (!InputManager.hasSeenKeyboardMouse) { - return Config.Couch - } - } - return Config.Desktop - } - return App.config.uiMode - } - - readonly property bool isCouchMode: effectiveUiMode === Config.Couch - readonly property real couchScale: isCouchMode ? 1.20 : 1.0 - - readonly property bool isMobile: deviceIsMobile - readonly property bool isTablet: deviceIsTablet - readonly property bool isTouchDevice: deviceIsTouchDevice - readonly property bool isNarrowScreen: width < Kirigami.Units.gridUnit * 30 - - readonly property int adaptiveCardSize: Math.round(App.config.gridSize * couchScale) - - pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar - pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn - - footer: Item { - visible: !root.isCouchMode - implicitWidth: root.width - - implicitHeight: footerBar.implicitHeight - height: implicitHeight - - QQC2.ToolBar { - id: footerBar + Image { + id: backdropImg anchors.fill: parent + source: root.focusedGame && root.focusedGame.coverUrl ? root.focusedGame.coverUrl : "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + visible: false + } - visible: true + MultiEffect { + source: backdropImg + anchors.fill: backdropImg + blurEnabled: true + blur: 1.0 + blurMax: 72 + brightness: -0.25 + opacity: 0.38 - leftPadding: 0 - rightPadding: 0 - topPadding: Kirigami.Units.smallSpacing - bottomPadding: Kirigami.Units.smallSpacing + Behavior on opacity { + NumberAnimation { duration: 600; easing.type: Easing.InOutCubic } + } + } - contentItem: Item { - id: footerRoot + Rectangle { + anchors.fill: parent + gradient: Gradient { + orientation: Gradient.Vertical + GradientStop { position: 0.0; color: Qt.rgba(0.05, 0.05, 0.08, 0.82) } + GradientStop { position: 0.4; color: Qt.rgba(0.05, 0.05, 0.08, 0.45) } + GradientStop { position: 1.0; color: Qt.rgba(0.05, 0.05, 0.08, 0.88) } + } + } + } - readonly property int footerMargins: root.isCouchMode ? Kirigami.Units.mediumSpacing : Kirigami.Units.smallSpacing - readonly property int footerMinHeight: Math.round(Kirigami.Units.gridUnit * (root.isCouchMode ? 2.2 : 1.6)) + ColumnLayout { + anchors.fill: parent + spacing: 0 + z: 1 - implicitHeight: Math.max(footerLayout.implicitHeight + footerMargins * 2, footerMinHeight) + Rectangle { + Layout.fillWidth: true + height: 1 + color: Qt.rgba(1, 1, 1, 0.07) + visible: !App.launcher.daemonAvailable + } - Kirigami.ShadowedRectangle { - id: footerBg + QQC2.Label { + Layout.fillWidth: true + visible: !App.launcher.daemonAvailable + padding: 10 + text: i18n("Game Center daemon unavailable — launch functionality limited") + color: "#ffcc44" + font.pixelSize: 13 + horizontalAlignment: Text.AlignHCenter + background: Rectangle { color: Qt.rgba(1, 0.8, 0.2, 0.12) } + } + + Rectangle { + Layout.fillWidth: true + height: 56 + color: Qt.rgba(0, 0, 0, 0.38) + + RowLayout { anchors.fill: parent - radius: Kirigami.Units.largeSpacing - color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.85) + anchors.leftMargin: 20 + anchors.rightMargin: 12 - shadow.size: Kirigami.Units.smallSpacing - shadow.color: Qt.rgba(0, 0, 0, 0.25) + Maui.Icon { + source: ":/icons/app/org.kde.alakarte.svg" + Layout.preferredWidth: 28 + Layout.preferredHeight: 28 + color: "transparent" + } - readonly property bool compactFooter: (hintBar.implicitWidth + rightExtras.implicitWidth + Kirigami.Units.gridUnit * 2) > width + Item { Layout.preferredWidth: 8 } - RowLayout { - id: footerLayout - anchors.fill: parent - anchors.margins: footerRoot.footerMargins - spacing: root.isCouchMode ? Kirigami.Units.largeSpacing : Kirigami.Units.mediumSpacing - - Flickable { - id: hintFlick - clip: true - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - Layout.minimumHeight: Math.max(hintBar.implicitHeight, footerRoot.footerMinHeight - footerRoot.footerMargins * 2) - Layout.preferredHeight: Layout.minimumHeight - - contentWidth: hintContent.width - contentHeight: hintContent.height - boundsBehavior: Flickable.StopAtBounds - flickableDirection: Flickable.HorizontalFlick - interactive: hintBar.implicitWidth > width - - QQC2.ScrollBar.horizontal: QQC2.ScrollBar { policy: QQC2.ScrollBar.AlwaysOff } - - Item { - id: hintContent - width: Math.max(hintBar.implicitWidth, hintFlick.width) - height: Math.max(hintBar.implicitHeight, hintFlick.height) - - BottomHintBar { - id: hintBar - context: root.hintContext - uiMode: root.effectiveUiMode - activeInput: InputManager.activeInput - - width: implicitWidth - height: implicitHeight - anchors.centerIn: parent - } - } + ConsoleCategoryRail { + id: categoryRail + Layout.fillWidth: true + Layout.fillHeight: true + currentCategory: root.currentCategory + onCategorySelected: function(cat) { + root.currentCategory = cat } + } + + QQC2.ToolButton { + id: searchBtn + icon.name: "edit-find" + icon.color: "white" + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + onClicked: root.searchVisible = !root.searchVisible + background: Rectangle { + radius: 6 + color: searchBtn.pressed ? Qt.rgba(1,1,1,0.18) : (searchBtn.hovered ? Qt.rgba(1,1,1,0.12) : "transparent") + } + } + + QQC2.ToolButton { + id: settingsBtn + icon.name: "configure" + icon.color: "white" + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + onClicked: root.settingsVisible = true + background: Rectangle { + radius: 6 + color: settingsBtn.pressed ? Qt.rgba(1,1,1,0.18) : (settingsBtn.hovered ? Qt.rgba(1,1,1,0.12) : "transparent") + } + } + + QQC2.ToolButton { + id: importBtn + icon.name: "document-import" + icon.color: "white" + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + onClicked: root.importVisible = true + background: Rectangle { + radius: 6 + color: importBtn.pressed ? Qt.rgba(1,1,1,0.18) : (importBtn.hovered ? Qt.rgba(1,1,1,0.12) : "transparent") + } + } + } + } + + QQC2.TextField { + id: searchField + Layout.fillWidth: true + Layout.preferredHeight: 44 + Layout.leftMargin: 24 + Layout.rightMargin: 24 + Layout.topMargin: 4 + visible: root.searchVisible + placeholderText: i18n("Search games…") + color: "white" + font.pixelSize: 15 + background: Rectangle { + radius: 8 + color: Qt.rgba(1, 1, 1, 0.10) + border.color: Qt.rgba(1, 1, 1, 0.22) + border.width: 1 + } + onTextChanged: root.searchText = text + Keys.onEscapePressed: { + root.searchVisible = false + root.searchText = "" + text = "" + libraryView.restoreFocus() + } + } + + LibraryView { + id: libraryView + Layout.fillWidth: true + Layout.fillHeight: true + filterSource: root.currentCategory + filterText: root.searchText + + onGameFocused: function(game) { + root.focusedGame = game + } + onGameSelected: function(game) { + root.selectedGame = game + root.detailVisible = true + } + onGameLaunched: function(game) { + App.launcher.launchGame(game) + } + } + + Rectangle { + Layout.fillWidth: true + height: 38 + color: Qt.rgba(0, 0, 0, 0.55) + visible: GamepadManager.connected + + RowLayout { + anchors.centerIn: parent + spacing: 32 + + Repeater { + model: [ + { icon: ":/icons/gamepad/generic/south.svg", label: i18n("Select / Details") }, + { icon: ":/icons/gamepad/generic/east.svg", label: i18n("Back") }, + { icon: ":/icons/gamepad/generic/lb.svg", label: i18n("Prev Category") }, + { icon: ":/icons/gamepad/generic/rb.svg", label: i18n("Next Category") }, + { icon: ":/icons/gamepad/generic/menu.svg", label: i18n("Settings") } + ] RowLayout { - id: rightExtras - spacing: Kirigami.Units.mediumSpacing + spacing: 5 - visible: !footerBg.compactFooter - && !root.isCouchMode - - 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 + Image { + source: modelData.icon + width: 18; height: 18 + fillMode: Image.PreserveAspectFit + smooth: true } - 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" - } + QQC2.Label { + text: modelData.label + color: Qt.rgba(1, 1, 1, 0.6) + font.pixelSize: 12 } } } } - } } } - Shortcut { - sequence: "Ctrl+F" - onActivated: { - if (root.currentConfirmDialog()) return - if (detailsSheet.opened || diagnosticsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened) return - if (sidebar.modal && sidebar.opened) return - searchActive = true - Qt.callLater(function() { libraryView.focusSearch() }) - } - } + ConsoleGameDetail { + id: detailView + anchors.fill: parent + game: root.selectedGame + visible: root.detailVisible + z: 10 - Shortcut { - sequence: "Ctrl+I" - onActivated: { - if (root.currentConfirmDialog()) return - importSheet.open() + onClose: { + root.detailVisible = false + root.selectedGame = null + libraryView.restoreFocus() } - } - - Shortcut { - sequence: "Ctrl+N" - onActivated: { - if (root.currentConfirmDialog()) return - gameEditDialog.game = null + onLaunch: { + App.launcher.launchGame(root.selectedGame) + root.detailVisible = false + root.selectedGame = null + libraryView.restoreFocus() + } + onEditRequested: { + root.detailVisible = false + gameEditDialog.game = root.selectedGame gameEditDialog.open() } - } - - Shortcut { - sequence: "Ctrl+," - onActivated: { - if (root.currentConfirmDialog()) return - if (settingsSheet.opened) { - settingsSheet.close() - return - } - if (settingsLayerOpen) { - root.closeSettings() - return - } - if (diagnosticsSheet.opened) { - diagnosticsSheet.close() - Qt.callLater(function() { root.openSettings() }) - return - } - if (root.canOpenSettings()) { - root.openSettings() - return - } - if (detailsSheet.opened) { - detailsSheet.close() - Qt.callLater(function() { root.openSettings() }) - return - } - if (sidebar.modal && sidebar.opened) { - sidebar.close() - Qt.callLater(function() { root.openSettings() }) - return - } + onRemoveRequested: { + root.detailVisible = false + root.selectedGame = null + libraryView.restoreFocus() } } - Shortcut { - sequence: "Escape" - onActivated: root.closeTopmost() - } - - Shortcut { - sequence: "F5" - onActivated: { - if (root.currentConfirmDialog()) return - App.importAllGames() - } - } - - Shortcut { - sequence: "Ctrl+H" - onActivated: { - if (root.currentConfirmDialog()) return - if (detailsSheet.opened || diagnosticsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened) return - if (root.hintContext !== "library" && root.hintContext !== "sidebar") return - let target = (root.currentSource === "hidden") ? "all" : "hidden" - sidebarView.suppressAutoClose = true - sidebarView.applySourceById(target) - Qt.callLater(function() { sidebarView.suppressAutoClose = false }) - } - } - - Shortcut { - sequence: "Ctrl+PgUp" - onActivated: { - if (root.currentConfirmDialog()) return - if (detailsSheet.opened || diagnosticsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened) return - if (root.hintContext !== "library" && root.hintContext !== "sidebar") return - sidebarView.cycleSource(-1) - } - } - - Shortcut { - sequence: "Ctrl+PgDown" - onActivated: { - if (root.currentConfirmDialog()) return - if (detailsSheet.opened || diagnosticsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened) return - if (root.hintContext !== "library" && root.hintContext !== "sidebar") return - sidebarView.cycleSource(1) - } - } - - Shortcut { - sequence: "Ctrl+D" - onActivated: { - if (root.currentConfirmDialog()) return - if (detailsSheet.opened || diagnosticsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened) return - if (sidebar.modal && sidebar.opened) return - if (root.selectedGame) { - detailsSheet.open() + Loader { + id: settingsLoader + anchors.fill: parent + z: 20 + active: root.settingsVisible + sourceComponent: Item { + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.72) + TapHandler { onTapped: root.settingsVisible = false } } + SettingsPage { + anchors.centerIn: parent + width: Math.min(parent.width - 48, 760) + height: Math.min(parent.height - 48, 680) + } + Keys.onEscapePressed: root.settingsVisible = false } + onLoaded: item.forceActiveFocus() } - Shortcut { - sequence: "F9" - onActivated: { - if (root.currentConfirmDialog()) return - root.cycleUiMode() + Loader { + id: importLoader + anchors.fill: parent + z: 20 + active: root.importVisible + sourceComponent: DiagnosticsSheet { + onClosed: root.importVisible = false } + onLoaded: item.forceActiveFocus() + } + + Maui.ContextualMenu { + id: gameEditDialog + property var game: null } Connections { target: GamepadManager - function onSelectPressed() { - let d = root.currentConfirmDialog() - if (!d) return - root.activateFocusedInDialog(d) - } + function onBackPressed() { - root.closeTopmost() - } - function onLeftBumperPressed() { - if (root.currentConfirmDialog()) return - if (detailsSheet.opened || diagnosticsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened) return - if (root.hintContext !== "library" && root.hintContext !== "sidebar") return - sidebarView.cycleSource(-1) - } - function onRightBumperPressed() { - if (root.currentConfirmDialog()) return - if (detailsSheet.opened || diagnosticsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened) return - if (root.hintContext !== "library" && root.hintContext !== "sidebar") return - sidebarView.cycleSource(1) - } - function onDetailsPressed() { - if (root.currentConfirmDialog()) return - if (gameEditDialog.visible || diagnosticsSheet.opened || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened || (sidebar.modal && sidebar.opened)) return - if (root.hintContext !== "library" && root.hintContext !== "details") return - if (detailsSheet.opened) { - if (root.selectedGame) { - root.selectedGame.favorite = !root.selectedGame.favorite - } + if (root.settingsVisible) { root.settingsVisible = false; return } + if (root.importVisible) { root.importVisible = false; return } + if (root.searchVisible) { + root.searchVisible = false + root.searchText = "" + searchField.text = "" + libraryView.restoreFocus() return } - if (!libraryView || !libraryView.visible) return - libraryView.openDetailsForFocusedGame() } + function onMenuPressed() { - if (root.isCouchMode) { - if (libraryView && libraryView.anyMenuOpen) { - libraryView.closeCurrentMenu() - return - } - if (removeGameConfirmDialog.opened) { - root.pendingSidebarOpen = true - removeGameConfirmDialog.close() - root.pendingRemoveGame = null - Qt.callLater(function() { libraryView.restoreFocus() }) - return - } - { - let layerContent = root.settingsLayerContentItem() - if (settingsLayerOpen && layerContent && layerContent.anyConfirmOpen) { - root.pendingSidebarOpen = true - root.closeSettings() - return - } - if (settingsLayerOpen && layerContent && layerContent.anyMenuOpen) { - layerContent.closeCurrentMenu() - return - } - } - if (settingsSheet.opened && settingsContent.anyConfirmOpen) { - root.pendingSidebarOpen = true - settingsSheet.close() - return - } - if (settingsSheet.opened && settingsContent.anyMenuOpen) { - settingsContent.closeCurrentMenu() - return - } - if (settingsSheet.opened) { - root.pendingSidebarOpen = true - settingsSheet.close() - return - } - if (settingsLayerOpen) { - root.pendingSidebarOpen = true - root.closeSettings() - return - } - if (gameEditDialog.visible) { - root.pendingSidebarOpen = true - gameEditDialog.close() - return - } - if (importSheet.opened) { - root.pendingSidebarOpen = true - importSheet.close() - return - } - if (aboutSheet.opened) { - root.pendingSidebarOpen = true - aboutSheet.close() - return - } - if (diagnosticsSheet.opened) { - root.pendingSidebarOpen = true - diagnosticsSheet.close() - return - } - if (detailsSheet.opened) { - root.pendingSidebarOpen = true - detailsSheet.close() - return - } - if (sidebar.modal) { - if (sidebar.opened) { - sidebar.close() - } else { - sidebar.open() - } - return - } - } - - if (settingsSheet.opened) { - settingsSheet.close() - return - } - - if (!settingsLayerOpen) { - if (root.canOpenSettings()) { - root.openSettings() - } else if (detailsSheet.opened) { - detailsSheet.close() - Qt.callLater(function() { root.openSettings() }) - } else { - root.closeTopmost() - } - } else { - root.closeSettings() - } + root.settingsVisible = !root.settingsVisible } + function onSearchPressed() { - if (root.currentConfirmDialog()) return - if (root.hintContext !== "library" && root.hintContext !== "details") return - if (detailsSheet.opened) { - detailsSheet.editRequested() - return - } - if (gameEditDialog.visible || diagnosticsSheet.opened || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened || (sidebar.modal && sidebar.opened)) return - searchActive = true - Qt.callLater(function() { libraryView.focusSearch() }) - } - function onNavigateUp() { - let d = root.currentConfirmDialog() - if (!d) return - root.focusNextInDialog(d, false) - } - function onNavigateDown() { - let d = root.currentConfirmDialog() - if (!d) return - root.focusNextInDialog(d, true) - } - function onNavigateLeft() { - let d = root.currentConfirmDialog() - if (!d) return - root.focusNextInDialog(d, false) - } - function onNavigateRight() { - let d = root.currentConfirmDialog() - if (!d) return - root.focusNextInDialog(d, true) - } - } - - Connections { - target: root.pageStack.layers - function onDepthChanged() { - if (root.pageStack.layers.depth <= 1) { - root.settingsLayerOpen = false - Qt.callLater(function() { libraryView.restoreFocus() }) - } - } - } - - globalDrawer: Kirigami.OverlayDrawer { - id: sidebar - edge: Qt.LeftEdge - modal: root.isMobile || root.isNarrowScreen || root.isCouchMode - enabled: !root.isCouchMode - handleClosedIcon.source: "application-menu" - handleOpenIcon.source: "go-previous" - handleVisible: root.isMobile || root.isNarrowScreen || root.isCouchMode - - 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: { - if (root.pendingAboutOpen) { - root.pendingAboutOpen = false - Qt.callLater(function() { aboutSheet.open() }) - return - } - 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) { - if (!sidebarView.suppressAutoClose) { - sidebar.close() - Qt.callLater(function() { libraryView.restoreFocus() }) - } - } - } - - onSettingsRequested: { - if (sidebar.modal) { - sidebar.close() - } - root.openSettings() - } - onImportRequested: { - if (sidebar.modal) { - sidebar.close() - } - importSheet.open() - } - onAboutRequested: { - if (sidebar.modal) { - root.pendingAboutOpen = true - sidebar.close() - return - } - aboutSheet.open() - } - } - } - } - - pageStack.initialPage: Kirigami.Page { - id: mainPage - title: sidebarView.currentSourceName - - padding: root.isCouchMode ? 0 : Kirigami.Units.largeSpacing - - // Couch Mode Dynamic Background - Item { - anchors.fill: parent - visible: root.isCouchMode - z: -1 - - Image { - id: globalBackgroundCover - anchors.fill: parent - source: libraryView.focusedCoverUrl - fillMode: Image.PreserveAspectCrop - asynchronous: true - smooth: true - mipmap: App.config.highQualityImages - opacity: 0.15 - - layer.enabled: true - layer.effect: MultiEffect { - blurEnabled: true - blur: 0.95 - blurMax: 80 - } - } - - Rectangle { - anchors.fill: parent - color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.8) - } - } - - 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.clearSearch() - 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") - visible: sidebar.modal - onTriggered: importSheet.open() - }, - Kirigami.Action { - icon.name: "configure" - text: i18n("Settings") - visible: sidebar.modal - onTriggered: root.openSettings() - } - ] - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - CouchSidebar { - id: couchSidebar - Layout.fillWidth: true - Layout.preferredHeight: Kirigami.Units.gridUnit * 4 - visible: root.isCouchMode - - currentSource: sidebarView.currentSource - - onSourceSelected: function(source) { - sidebarView.applySourceById(source) - } - - onSettingsRequested: { - root.openSettings() - } - onImportRequested: { - importSheet.open() - } - onAboutRequested: { - aboutSheet.open() - } - } - - Kirigami.InlineMessage { - id: daemonBanner - Layout.fillWidth: true - Layout.bottomMargin: Kirigami.Units.smallSpacing - type: Kirigami.MessageType.Error - visible: !App.launcher.daemonAvailable - text: i18n("Game Center daemon is not running. Game launching is unavailable.") - actions: [ - Kirigami.Action { - text: i18n("Retry") - icon.name: "view-refresh" - onTriggered: App.launcher.retryDaemonConnection() - } - ] - } - - LibraryView { - id: libraryView - Layout.fillWidth: true - Layout.fillHeight: true - - 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() - } - - onDiagnosticsRequested: { - diagnosticsSheet.game = root.selectedGame - diagnosticsSheet.open() - } - - onRemoveRequested: { - if (!root.selectedGame) return - root.pendingRemoveGame = root.selectedGame - removeGameConfirmDialog.open() - } - - onClosed: { - libraryView.restoreFocus() - } - } - - DiagnosticsSheet { - id: diagnosticsSheet - game: root.selectedGame - - onClosed: { - if (detailsSheet.opened) { - detailsSheet.forceActiveFocus() - return - } - libraryView.restoreFocus() - } - } - - Kirigami.PromptDialog { - id: removeGameConfirmDialog - title: i18n("Remove Game") - subtitle: root.pendingRemoveGame - ? i18n("Are you sure you want to remove '%1'?", root.pendingRemoveGame.name) - : i18n("Are you sure you want to remove this game?") - standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No - onOpened: { - Qt.callLater(function() { - if (typeof removeGameConfirmDialog.standardButton === "function") { - let noButton = removeGameConfirmDialog.standardButton(Kirigami.Dialog.No) - if (noButton) { - noButton.forceActiveFocus() - return - } - } - removeGameConfirmDialog.forceActiveFocus() - }) - } - onAccepted: { - if (!root.pendingRemoveGame) return - let gameId = root.pendingRemoveGame.id - let gameName = root.pendingRemoveGame.name - App.removeGame(root.pendingRemoveGame) - root.pendingRemoveGame = null - detailsSheet.close() - root.selectedGame = null - showPassiveNotification( - i18n("%1 removed", gameName), - "long", - i18n("Undo"), - function() { App.restoreGame(gameId) } - ) - } - onRejected: { - root.pendingRemoveGame = null - } - } - - 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 - if (ancestor.contentItem && p === ancestor.contentItem) return true - if (ancestor.header && p === ancestor.header) return true - if (p.visualParent !== undefined && p.visualParent !== null) { - if (importSheet.isDescendant(p.visualParent, ancestor)) return true - } else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) { - if (importSheet.isDescendant(p.popup.visualParent, 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 - } + root.searchVisible = !root.searchVisible + if (root.searchVisible) { + Qt.callLater(function() { searchField.forceActiveFocus() }) } } - function activateFocused() { - let w = applicationWindow() - if (!w || !w.activeFocusItem) return - let item = w.activeFocusItem - if (typeof item.toggle === "function") { - item.toggle() - return - } - if (item.checkable !== undefined && item.checkable && item.checked !== undefined) { - item.checked = !item.checked - return - } - if (typeof item.clicked === "function") { - item.clicked() - return - } + function onLeftBumperPressed() { + if (!root.detailVisible) categoryRail.selectPrevious() } - Connections { - target: GamepadManager - function onNavigateUp() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - 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 onNavigateDown() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (!importSheet.opened || importScroll.activeFocus) return - let w = applicationWindow() - if (!w || !w.activeFocusItem) return - if (!importSheet.isDescendant(w.activeFocusItem, importSheet)) return - importSheet.focusNextInChain(true) - } - function onNavigateLeft() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - 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() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (!importSheet.opened || importScroll.activeFocus) return - let w = applicationWindow() - if (!w || !w.activeFocusItem) return - if (!importSheet.isDescendant(w.activeFocusItem, importSheet)) return - importSheet.focusNextInChain(true) - } - function onSelectPressed() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (!importSheet.opened || importScroll.activeFocus) return - let w = applicationWindow() - if (!w || !w.activeFocusItem) return - if (!importSheet.isDescendant(w.activeFocusItem, importSheet)) 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() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (importScroll.activeFocus) importScroll.scrollBy(-Kirigami.Units.gridUnit * 2) - } - function onNavigateDown() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - 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 && App.config.importSteam && !App.gameModel.hasPlatformPrefix("Steam") && App.gameModel.count >= 0 - 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 && App.config.importLutris && !App.gameModel.hasPlatformPrefix("Lutris") && App.gameModel.count >= 0 - 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 && App.config.importHeroic && !App.gameModel.hasPlatformPrefix("Heroic") && App.gameModel.count >= 0 - 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 && App.config.importDesktop && !App.gameModel.hasPlatformPrefix("Desktop") && App.gameModel.count >= 0 - 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 && App.config.importBottles && !App.gameModel.hasPlatformPrefix("Bottles") && App.gameModel.count >= 0 - 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 && App.config.importFlatpak && !App.gameModel.hasPlatformPrefix("Flatpak") && App.gameModel.count >= 0 - 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 && App.config.importItch && !App.gameModel.hasPlatformPrefix("itch.io") && App.gameModel.count >= 0 - 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 && App.config.importLegendary && !App.gameModel.hasPlatformPrefix("Legendary") && App.gameModel.count >= 0 - 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 && App.config.importRetroArch && !App.gameModel.hasPlatformPrefix("RetroArch") && App.gameModel.count >= 0 - 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 && 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"))) - onClicked: App.importAllGames() - } - - QQC2.Button { - text: i18n("Close") - onClicked: importSheet.close() - } - } - } - } - - Component { - id: settingsLayerComponent - - Kirigami.Page { - id: settingsLayerPage - title: i18n("Settings") - - property Item content: settingsLayerContent - - 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 - if (p.visualParent !== undefined && p.visualParent !== null) { - if (settingsLayerPage.isDescendant(p.visualParent, ancestor)) return true - } else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) { - if (settingsLayerPage.isDescendant(p.popup.visualParent, 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.isActualDescendant(next, settingsLayerContent)) { - settingsLayerScroll.ensureItemVisible(next) - } - return - } - } - } - - function isActualDescendant(item, ancestor) { - let p = item - while (p) { - if (p === ancestor) return true - p = p.parent - } - return false - } - - function activateFocused() { - let w = applicationWindow() - if (!w || !w.activeFocusItem) return - let item = w.activeFocusItem - if (typeof item.toggle === "function") { - item.toggle() - return - } - if (item.checkable !== undefined && item.checkable && item.checked !== undefined) { - item.checked = !item.checked - return - } - if (typeof item.clicked === "function") { - item.clicked() - return - } - } - - Connections { - target: GamepadManager - function onNavigateUp() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (settingsLayerContent.anyConfirmOpen) return - if (settingsLayerContent.anyMenuOpen) return - 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() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (settingsLayerContent.anyConfirmOpen) return - if (settingsLayerContent.anyMenuOpen) return - 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 onNavigateLeft() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (settingsLayerContent.anyConfirmOpen) return - if (settingsLayerContent.anyMenuOpen) return - 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 onNavigateRight() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (settingsLayerContent.anyConfirmOpen) return - if (settingsLayerContent.anyMenuOpen) return - 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() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (settingsLayerContent.anyConfirmOpen) return - if (settingsLayerContent.anyMenuOpen) return - 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() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (root.settingsLayerOpen && settingsLayerScroll.activeFocus) settingsLayerScroll.scrollBy(-Kirigami.Units.gridUnit * 2) - } - function onNavigateDown() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (root.settingsLayerOpen && settingsLayerScroll.activeFocus) settingsLayerScroll.scrollBy(Kirigami.Units.gridUnit * 2) - } - } - - SettingsPage { - id: settingsLayerContent - width: settingsLayerScroll.availableWidth - } - } - } - } - - Kirigami.OverlaySheet { - id: settingsSheet - title: i18n("Settings") - - 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) - - closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnPressOutside - - 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))) - } - 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 - } - - onOpened: settingsContent.focusFirstControl() - onClosed: libraryView.restoreFocus() - - header: Kirigami.ShadowedRectangle { - id: settingsHeader - implicitWidth: settingsSheet.width - 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 - } - } - } - - 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 - if (p.visualParent !== undefined && p.visualParent !== null) { - if (settingsSheet.isDescendant(p.visualParent, ancestor)) return true - } else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) { - if (settingsSheet.isDescendant(p.popup.visualParent, 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, settingsSheet)) { - next.forceActiveFocus() - if (settingsSheet.isActualDescendant(next, settingsContent)) { - settingsScroll.ensureItemVisible(next) - } - return - } - } - } - - function isActualDescendant(item, ancestor) { - let p = item - while (p) { - if (p === ancestor) return true - p = p.parent - } - return false - } - - function activateFocused() { - let w = applicationWindow() - if (!w || !w.activeFocusItem) return - let item = w.activeFocusItem - if (typeof item.toggle === "function") { - item.toggle() - return - } - if (item.checkable !== undefined && item.checkable && item.checked !== undefined) { - item.checked = !item.checked - return - } - if (typeof item.clicked === "function") { - item.clicked() - return - } - } - - Connections { - target: GamepadManager - function onNavigateUp() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (settingsContent.anyConfirmOpen) return - if (settingsContent.anyMenuOpen) return - if (!settingsSheet.opened || settingsScroll.activeFocus) return - let w = applicationWindow() - if (!w || !w.activeFocusItem) return - if (!settingsSheet.isDescendant(w.activeFocusItem, settingsSheet)) return - settingsSheet.focusNextInChain(false) - } - function onNavigateDown() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (settingsContent.anyConfirmOpen) return - if (settingsContent.anyMenuOpen) return - if (!settingsSheet.opened || settingsScroll.activeFocus) return - let w = applicationWindow() - if (!w || !w.activeFocusItem) return - if (!settingsSheet.isDescendant(w.activeFocusItem, settingsSheet)) return - settingsSheet.focusNextInChain(true) - } - function onNavigateLeft() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (settingsContent.anyConfirmOpen) return - if (settingsContent.anyMenuOpen) return - if (!settingsSheet.opened || settingsScroll.activeFocus) return - let w = applicationWindow() - if (!w || !w.activeFocusItem) return - if (!settingsSheet.isDescendant(w.activeFocusItem, settingsSheet)) return - settingsSheet.focusNextInChain(false) - } - function onNavigateRight() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (settingsContent.anyConfirmOpen) return - if (settingsContent.anyMenuOpen) return - if (!settingsSheet.opened || settingsScroll.activeFocus) return - let w = applicationWindow() - if (!w || !w.activeFocusItem) return - if (!settingsSheet.isDescendant(w.activeFocusItem, settingsSheet)) return - settingsSheet.focusNextInChain(true) - } - function onSelectPressed() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (settingsContent.anyConfirmOpen) return - if (settingsContent.anyMenuOpen) return - if (!settingsSheet.opened || settingsScroll.activeFocus) return - let w = applicationWindow() - if (!w || !w.activeFocusItem) return - if (!settingsSheet.isDescendant(w.activeFocusItem, settingsSheet)) return - settingsSheet.activateFocused() - } - } - - contentItem: QQC2.ScrollView { - id: settingsScroll - 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 || !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() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (settingsScroll.activeFocus) settingsScroll.scrollBy(-Kirigami.Units.gridUnit * 2) - } - function onNavigateDown() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (settingsScroll.activeFocus) settingsScroll.scrollBy(Kirigami.Units.gridUnit * 2) - } - } - - SettingsPage { - id: settingsContent - width: settingsScroll.availableWidth - } - } - } - - Kirigami.OverlaySheet { - id: aboutSheet - title: i18n("About") - - parent: applicationWindow().overlay - - 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) - } - - onOpened: aboutCloseButton.forceActiveFocus() - onClosed: libraryView.restoreFocus() - - 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 - if (p.visualParent !== undefined && p.visualParent !== null) { - if (aboutSheet.isDescendant(p.visualParent, ancestor)) return true - } else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) { - if (aboutSheet.isDescendant(p.popup.visualParent, 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 (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.checkable !== undefined && item.checkable && item.checked !== undefined) { - item.checked = !item.checked - return - } - if (typeof item.clicked === "function") { - item.clicked() - return - } - } - - Connections { - target: GamepadManager - function onNavigateUp() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (!aboutSheet.opened) return - let w = applicationWindow() - if (!w || !w.activeFocusItem) return - if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return - aboutSheet.focusNextInChain(false) - } - function onNavigateDown() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (!aboutSheet.opened) return - let w = applicationWindow() - if (!w || !w.activeFocusItem) return - if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return - aboutSheet.focusNextInChain(true) - } - function onNavigateLeft() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (!aboutSheet.opened) return - let w = applicationWindow() - if (!w || !w.activeFocusItem) return - if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return - aboutSheet.focusNextInChain(false) - } - function onNavigateRight() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (!aboutSheet.opened) return - let w = applicationWindow() - if (!w || !w.activeFocusItem) return - if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return - aboutSheet.focusNextInChain(true) - } - function onSelectPressed() { - let app = applicationWindow() - if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return - if (!aboutSheet.opened) return - let w = applicationWindow() - if (!w || !w.activeFocusItem) return - if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return - aboutSheet.activateFocused() - } - } - - 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 { - id: aboutCloseButton - 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" - }) - } - } - } - } - - 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)) + function onRightBumperPressed() { + if (!root.detailVisible) categoryRail.selectNext() } } 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") - } - } - - Connections { - target: App.runnerManager - - function onLastErrorChanged() { - if (!App.runnerManager) return - let msg = String(App.runnerManager.lastError || "") - if (msg.trim() === "") return - showPassiveNotification(i18n("Runner error: %1", msg), "long") - } - } - - Component.onCompleted: { - if (!root.isMobile) { - const savedW = App.config.windowWidth - const savedH = App.config.windowHeight - if (savedW > 0) root.width = Math.max(root.minimumWidth, savedW) - if (savedH > 0) root.height = Math.max(root.minimumHeight, savedH) - root.windowGeometryRestored = true - if (App.config.windowMaximized) { - root.showMaximized() - } - } else { - root.windowGeometryRestored = true - } - - if (App.gameModel.count === 0) { - importSheet.open() - } + function onImportingChanged() {} } } - -} diff --git a/third-party/mauikit b/third-party/mauikit new file mode 160000 index 0000000..32dbb34 --- /dev/null +++ b/third-party/mauikit @@ -0,0 +1 @@ +Subproject commit 32dbb346861fa89a1df9dfb82e54ff952754b232 diff --git a/third-party/mauiman b/third-party/mauiman new file mode 160000 index 0000000..6ad438d --- /dev/null +++ b/third-party/mauiman @@ -0,0 +1 @@ +Subproject commit 6ad438d97e000158aa0eda13ab669c208e804b0a