a-la-karte/src/qml/GameCard.qml

370 lines
13 KiB
QML
Raw Normal View History

// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024 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.kde.alakarte
FocusScope {
id: gameCard
property var game
property bool showPlayButton: true
property bool focused: activeFocus
readonly property bool isTouchDevice: {
let w = applicationWindow()
if (w && w.isTouchDevice !== undefined) return w.isTouchDevice
return Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile
}
Add Plasma tray, notification inhibition, session control, and flatpak runner Expand TrayController with daemon health polling every 10 seconds via D-Bus Ping calls to org.kde.GameCenter1, org.kde.ALaKarte.Runner1, and org.kde.ALaKarte.Input1. Add menu actions to toggle console behaviors, notification mirroring, and to restart each daemon via systemctl --user. Add NotificationInhibitor, owned by App, which calls org.freedesktop.Notifications Inhibit/UnInhibit on the session bus whenever the consoleBehaviors config setting changes. The cookie is released on application quit. Add consoleBehaviors and mirrorNotifications properties to Config with KConfig persistence under the [Console] group. Ship org.kde.alakarte.notifyrc defining GameLaunched, GameExited, and LaunchFailed notification events so Plasma attributes them correctly in the notification history. Extend RunnerManagerDaemon::ResolveLaunch with a flatpak runner branch that constructs a flatpak run command, translates environment overrides to --env= arguments, and respects flatpakAppId, flatpakBranch, flatpakArch, and flatpakArgs from the launch spec. Add activate_session, switch_to_vt, and terminate_session actions to the gamecenter KAuth helper and its polkit policy. Each action calls the corresponding method on org.freedesktop.login1 over the system bus. Add CouchSidebar.qml, a horizontal source tab bar shown in couch mode above the library view, exposing the same sourceSelected, settingsRequested, importRequested, and aboutRequested signals as SidebarView. Fix duplicate adaptiveFocusRingWidth property in GameCard.qml.
2026-03-22 15:53:09 +00:00
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 useAnimatedCover: App.config.animatedCovers
&& game
&& game.coverUrl
&& game.coverUrl.toString().toLowerCase().endsWith(".gif")
readonly property int coverStatus: useAnimatedCover ? animatedCover.status : staticCover.status
signal clicked()
signal doubleClicked()
signal playClicked()
Kirigami.ShadowedRectangle {
id: cardBackground
anchors.fill: parent
radius: Kirigami.Units.mediumSpacing
color: Kirigami.Theme.backgroundColor
shadow {
Add Plasma tray, notification inhibition, session control, and flatpak runner Expand TrayController with daemon health polling every 10 seconds via D-Bus Ping calls to org.kde.GameCenter1, org.kde.ALaKarte.Runner1, and org.kde.ALaKarte.Input1. Add menu actions to toggle console behaviors, notification mirroring, and to restart each daemon via systemctl --user. Add NotificationInhibitor, owned by App, which calls org.freedesktop.Notifications Inhibit/UnInhibit on the session bus whenever the consoleBehaviors config setting changes. The cookie is released on application quit. Add consoleBehaviors and mirrorNotifications properties to Config with KConfig persistence under the [Console] group. Ship org.kde.alakarte.notifyrc defining GameLaunched, GameExited, and LaunchFailed notification events so Plasma attributes them correctly in the notification history. Extend RunnerManagerDaemon::ResolveLaunch with a flatpak runner branch that constructs a flatpak run command, translates environment overrides to --env= arguments, and respects flatpakAppId, flatpakBranch, flatpakArch, and flatpakArgs from the launch spec. Add activate_session, switch_to_vt, and terminate_session actions to the gamecenter KAuth helper and its polkit policy. Each action calls the corresponding method on org.freedesktop.login1 over the system bus. Add CouchSidebar.qml, a horizontal source tab bar shown in couch mode above the library view, exposing the same sourceSelected, settingsRequested, importRequested, and aboutRequested signals as SidebarView. Fix duplicate adaptiveFocusRingWidth property in GameCard.qml.
2026-03-22 15:53:09 +00:00
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)
Behavior on scale {
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
}
Rectangle {
anchors.fill: parent
radius: Kirigami.Units.mediumSpacing
color: "transparent"
border.width: 2
border.color: Kirigami.Theme.highlightColor
opacity: 0.0
visible: gameCard.focused
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 }
}
}
// 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
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 {
anchors.centerIn: parent
source: "applications-games"
width: parent.width * 0.4
height: width
color: Kirigami.Theme.disabledTextColor
}
}
// Gradient overlay for text
Rectangle {
Add Plasma tray, notification inhibition, session control, and flatpak runner Expand TrayController with daemon health polling every 10 seconds via D-Bus Ping calls to org.kde.GameCenter1, org.kde.ALaKarte.Runner1, and org.kde.ALaKarte.Input1. Add menu actions to toggle console behaviors, notification mirroring, and to restart each daemon via systemctl --user. Add NotificationInhibitor, owned by App, which calls org.freedesktop.Notifications Inhibit/UnInhibit on the session bus whenever the consoleBehaviors config setting changes. The cookie is released on application quit. Add consoleBehaviors and mirrorNotifications properties to Config with KConfig persistence under the [Console] group. Ship org.kde.alakarte.notifyrc defining GameLaunched, GameExited, and LaunchFailed notification events so Plasma attributes them correctly in the notification history. Extend RunnerManagerDaemon::ResolveLaunch with a flatpak runner branch that constructs a flatpak run command, translates environment overrides to --env= arguments, and respects flatpakAppId, flatpakBranch, flatpakArch, and flatpakArgs from the launch spec. Add activate_session, switch_to_vt, and terminate_session actions to the gamecenter KAuth helper and its polkit policy. Each action calls the corresponding method on org.freedesktop.login1 over the system bus. Add CouchSidebar.qml, a horizontal source tab bar shown in couch mode above the library view, exposing the same sourceSelected, settingsRequested, importRequested, and aboutRequested signals as SidebarView. Fix duplicate adaptiveFocusRingWidth property in GameCard.qml.
2026-03-22 15:53:09 +00:00
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) }
}
}
// Game title
ColumnLayout {
Add Plasma tray, notification inhibition, session control, and flatpak runner Expand TrayController with daemon health polling every 10 seconds via D-Bus Ping calls to org.kde.GameCenter1, org.kde.ALaKarte.Runner1, and org.kde.ALaKarte.Input1. Add menu actions to toggle console behaviors, notification mirroring, and to restart each daemon via systemctl --user. Add NotificationInhibitor, owned by App, which calls org.freedesktop.Notifications Inhibit/UnInhibit on the session bus whenever the consoleBehaviors config setting changes. The cookie is released on application quit. Add consoleBehaviors and mirrorNotifications properties to Config with KConfig persistence under the [Console] group. Ship org.kde.alakarte.notifyrc defining GameLaunched, GameExited, and LaunchFailed notification events so Plasma attributes them correctly in the notification history. Extend RunnerManagerDaemon::ResolveLaunch with a flatpak runner branch that constructs a flatpak run command, translates environment overrides to --env= arguments, and respects flatpakAppId, flatpakBranch, flatpakArch, and flatpakArgs from the launch spec. Add activate_session, switch_to_vt, and terminate_session actions to the gamecenter KAuth helper and its polkit policy. Each action calls the corresponding method on org.freedesktop.login1 over the system bus. Add CouchSidebar.qml, a horizontal source tab bar shown in couch mode above the library view, exposing the same sourceSelected, settingsRequested, importRequested, and aboutRequested signals as SidebarView. Fix duplicate adaptiveFocusRingWidth property in GameCard.qml.
2026-03-22 15:53:09 +00:00
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
text: game ? game.name : ""
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize
color: "white"
elide: Text.ElideRight
wrapMode: Text.WordWrap
maximumLineCount: 2
}
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()
}
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onDoubleTapped: {
gameCard.doubleClicked()
gameCard.playClicked()
}
}
// Context menu
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: contextMenu.popup()
}
QQC2.Menu {
id: contextMenu
QQC2.MenuItem {
text: i18n("Play")
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"
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"
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
}
}