mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-03-26 17:03:08 +00:00
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
This commit is contained in:
parent
e592e7b093
commit
a0b9ea1832
9 changed files with 1083 additions and 2631 deletions
|
|
@ -107,6 +107,8 @@ find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS
|
||||||
WindowSystem
|
WindowSystem
|
||||||
)
|
)
|
||||||
|
|
||||||
|
find_package(Qt6 COMPONENTS Multimedia QUIET)
|
||||||
|
|
||||||
find_package(KF6Auth ${KF_MIN_VERSION} QUIET)
|
find_package(KF6Auth ${KF_MIN_VERSION} QUIET)
|
||||||
|
|
||||||
find_package(KF6StatusNotifierItem ${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)
|
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
|
||||||
|
"$<BUILD_INTERFACE:${_mauiman_shim}>")
|
||||||
|
|
||||||
|
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)
|
||||||
add_subdirectory(src/krunner)
|
add_subdirectory(src/krunner)
|
||||||
add_subdirectory(icons)
|
add_subdirectory(icons)
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ target_link_libraries(alakarte PRIVATE
|
||||||
SDL3::SDL3
|
SDL3::SDL3
|
||||||
KF6::Kirigami
|
KF6::Kirigami
|
||||||
KF6::I18n
|
KF6::I18n
|
||||||
|
MauiKit4
|
||||||
KF6::CoreAddons
|
KF6::CoreAddons
|
||||||
KF6::ConfigCore
|
KF6::ConfigCore
|
||||||
KF6::ConfigGui
|
KF6::ConfigGui
|
||||||
|
|
@ -104,8 +105,10 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte
|
||||||
QML_FILES
|
QML_FILES
|
||||||
qml/Main.qml
|
qml/Main.qml
|
||||||
qml/LibraryView.qml
|
qml/LibraryView.qml
|
||||||
|
qml/GameCard.qml
|
||||||
|
qml/ConsoleGameDetail.qml
|
||||||
|
qml/ConsoleCategoryRail.qml
|
||||||
qml/CouchSidebar.qml
|
qml/CouchSidebar.qml
|
||||||
qml/GameCard.qml
|
|
||||||
qml/GameDetailsSheet.qml
|
qml/GameDetailsSheet.qml
|
||||||
qml/DiagnosticsSheet.qml
|
qml/DiagnosticsSheet.qml
|
||||||
qml/SettingsPage.qml
|
qml/SettingsPage.qml
|
||||||
|
|
|
||||||
161
src/qml/ConsoleCategoryRail.qml
Normal file
161
src/qml/ConsoleCategoryRail.qml
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
379
src/qml/ConsoleGameDetail.qml
Normal file
379
src/qml/ConsoleGameDetail.qml
Normal file
|
|
@ -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() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,314 +1,168 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// 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
|
||||||
import QtQuick.Controls as QQC2
|
import QtQuick.Controls as QQC2
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import org.kde.kirigami as Kirigami
|
import org.mauikit.controls as Maui
|
||||||
import org.kde.alakarte
|
import org.kde.alakarte
|
||||||
|
|
||||||
FocusScope {
|
Item {
|
||||||
id: gameCard
|
id: gameCard
|
||||||
|
|
||||||
property var game
|
property var game
|
||||||
property bool showPlayButton: true
|
signal clicked()
|
||||||
property bool focused: activeFocus
|
signal playClicked()
|
||||||
|
|
||||||
readonly property bool isTouchDevice: {
|
readonly property bool isCurrent: GridView.isCurrentItem
|
||||||
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 useAnimatedCover: App.config.animatedCovers
|
readonly property bool useAnimatedCover: App.config.animatedCovers
|
||||||
&& game
|
&& game && game.coverUrl
|
||||||
&& game.coverUrl
|
|
||||||
&& game.coverUrl.toString().toLowerCase().endsWith(".gif")
|
&& game.coverUrl.toString().toLowerCase().endsWith(".gif")
|
||||||
|
|
||||||
readonly property int coverStatus: useAnimatedCover ? animatedCover.status : staticCover.status
|
readonly property int coverStatus: useAnimatedCover ? animatedCover.status : staticCover.status
|
||||||
|
|
||||||
signal clicked()
|
width: GridView.view ? GridView.view.cellWidth : 200
|
||||||
signal doubleClicked()
|
height: GridView.view ? GridView.view.cellHeight : 300
|
||||||
signal playClicked()
|
|
||||||
|
|
||||||
Kirigami.ShadowedRectangle {
|
Item {
|
||||||
id: cardBackground
|
id: cardContainer
|
||||||
anchors.fill: parent
|
anchors.centerIn: parent
|
||||||
radius: Kirigami.Units.mediumSpacing
|
width: parent.width - 16
|
||||||
|
height: parent.height - 16
|
||||||
|
|
||||||
color: Kirigami.Theme.backgroundColor
|
scale: gameCard.isCurrent ? 1.08 : (hoverHandler.hovered ? 1.03 : 1.0)
|
||||||
|
|
||||||
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)
|
|
||||||
Behavior on scale {
|
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 {
|
Rectangle {
|
||||||
|
id: coverFrame
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: Kirigami.Units.mediumSpacing
|
radius: 8
|
||||||
color: "transparent"
|
color: "#1a1a2a"
|
||||||
border.width: 2
|
clip: true
|
||||||
border.color: Kirigami.Theme.highlightColor
|
|
||||||
opacity: 0.0
|
|
||||||
visible: gameCard.focused
|
|
||||||
|
|
||||||
SequentialAnimation on opacity {
|
Image {
|
||||||
running: gameCard.focused
|
id: staticCover
|
||||||
loops: Animation.Infinite
|
anchors.fill: parent
|
||||||
NumberAnimation { from: 0.10; to: 0.28; duration: 900; easing.type: Easing.InOutQuad }
|
source: game ? game.coverUrl : ""
|
||||||
NumberAnimation { from: 0.28; to: 0.12; duration: 900; easing.type: Easing.InOutQuad }
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
asynchronous: true
|
||||||
|
visible: !gameCard.useAnimatedCover
|
||||||
|
smooth: true
|
||||||
|
mipmap: true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Cover image
|
AnimatedImage {
|
||||||
Image {
|
id: animatedCover
|
||||||
id: staticCover
|
anchors.fill: parent
|
||||||
anchors.fill: parent
|
source: game ? game.coverUrl : ""
|
||||||
anchors.margins: Kirigami.Units.smallSpacing
|
fillMode: Image.PreserveAspectCrop
|
||||||
source: game ? game.coverUrl : ""
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
playing: gameCard.isCurrent
|
||||||
asynchronous: true
|
visible: gameCard.useAnimatedCover
|
||||||
visible: !gameCard.useAnimatedCover
|
smooth: true
|
||||||
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 {
|
Maui.Icon {
|
||||||
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
|
anchors.centerIn: parent
|
||||||
source: "applications-games"
|
source: "applications-games"
|
||||||
width: parent.width * 0.4
|
width: parent.width * 0.38
|
||||||
height: width
|
height: width
|
||||||
color: Kirigami.Theme.disabledTextColor
|
color: "#44ffffff"
|
||||||
|
visible: gameCard.coverStatus !== Image.Ready
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Gradient overlay for text
|
Rectangle {
|
||||||
Rectangle {
|
anchors.left: parent.left
|
||||||
opacity: (!root.isCouchMode || gameCard.focused || hoverHandler.hovered) ? 1.0 : 0.0
|
anchors.right: parent.right
|
||||||
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } }
|
anchors.bottom: parent.bottom
|
||||||
anchors.left: parent.left
|
height: parent.height * 0.42
|
||||||
anchors.right: parent.right
|
gradient: Gradient {
|
||||||
anchors.bottom: parent.bottom
|
GradientStop { position: 0.0; color: "transparent" }
|
||||||
anchors.margins: Kirigami.Units.smallSpacing
|
GradientStop { position: 0.55; color: Qt.rgba(0, 0, 0, 0.62) }
|
||||||
height: parent.height * 0.4
|
GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.92) }
|
||||||
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 {
|
|
||||||
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 {
|
QQC2.Label {
|
||||||
Layout.fillWidth: true
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
bottom: parent.bottom
|
||||||
|
margins: 10
|
||||||
|
}
|
||||||
text: game ? game.name : ""
|
text: game ? game.name : ""
|
||||||
font.bold: true
|
|
||||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize
|
|
||||||
color: "white"
|
color: "white"
|
||||||
|
font.pixelSize: 13
|
||||||
|
font.weight: Font.Medium
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
maximumLineCount: 2
|
maximumLineCount: 2
|
||||||
|
opacity: gameCard.isCurrent || hoverHandler.hovered ? 1.0 : 0.85
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
anchors.top: parent.top
|
||||||
spacing: Kirigami.Units.smallSpacing
|
anchors.left: parent.left
|
||||||
visible: App.config.showPlatformBadges
|
anchors.margins: 8
|
||||||
|
width: 9
|
||||||
Rectangle {
|
height: 9
|
||||||
implicitWidth: platformLabel.implicitWidth + Kirigami.Units.largeSpacing
|
radius: 5
|
||||||
implicitHeight: platformLabel.implicitHeight + Kirigami.Units.smallSpacing
|
color: "#4caf50"
|
||||||
Layout.preferredWidth: implicitWidth
|
visible: game && game.running
|
||||||
Layout.preferredHeight: implicitHeight
|
SequentialAnimation on opacity {
|
||||||
radius: Kirigami.Units.smallSpacing
|
running: game && game.running
|
||||||
color: getPlatformColor(game ? game.platform : "")
|
loops: Animation.Infinite
|
||||||
|
NumberAnimation { to: 0.25; duration: 700 }
|
||||||
QQC2.Label {
|
NumberAnimation { to: 1.0; duration: 700 }
|
||||||
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 {
|
Maui.Icon {
|
||||||
acceptedButtons: Qt.LeftButton
|
anchors.top: parent.top
|
||||||
onDoubleTapped: {
|
anchors.right: parent.right
|
||||||
gameCard.doubleClicked()
|
anchors.margins: 8
|
||||||
gameCard.playClicked()
|
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 {
|
TapHandler {
|
||||||
acceptedButtons: Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
onTapped: contextMenu.popup()
|
onTapped: contextMenu.popup()
|
||||||
|
|
@ -322,48 +176,23 @@ FocusScope {
|
||||||
icon.name: "media-playback-start"
|
icon.name: "media-playback-start"
|
||||||
onTriggered: gameCard.playClicked()
|
onTriggered: gameCard.playClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.MenuSeparator {}
|
QQC2.MenuSeparator {}
|
||||||
|
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
text: game && game.favorite ? i18n("Remove from Favorites") : i18n("Add to Favorites")
|
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
|
onTriggered: if (game) game.favorite = !game.favorite
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
text: game && game.hidden ? i18n("Show in Library") : i18n("Hide from Library")
|
text: game && game.hidden ? i18n("Show in Library") : i18n("Hide from Library")
|
||||||
icon.name: game && game.hidden ? "view-visible" : "view-hidden"
|
icon.name: game && game.hidden ? "view-visible" : "view-hidden"
|
||||||
onTriggered: if (game) game.hidden = !game.hidden
|
onTriggered: if (game) game.hidden = !game.hidden
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.MenuSeparator {}
|
QQC2.MenuSeparator {}
|
||||||
|
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
text: i18n("View Details")
|
text: i18n("View Details")
|
||||||
icon.name: "documentation"
|
icon.name: "documentinfo"
|
||||||
onTriggered: gameCard.clicked()
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// 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
|
||||||
import QtQuick.Controls as QQC2
|
import QtQuick.Controls as QQC2
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Effects
|
import org.mauikit.controls as Maui
|
||||||
import org.kde.kirigami as Kirigami
|
|
||||||
import org.kde.alakarte
|
import org.kde.alakarte
|
||||||
import "components"
|
import "components"
|
||||||
|
|
||||||
|
|
@ -13,321 +12,125 @@ FocusScope {
|
||||||
id: libraryRoot
|
id: libraryRoot
|
||||||
|
|
||||||
property string filterSource: "all"
|
property string filterSource: "all"
|
||||||
property bool searchActive: false
|
property string filterText: ""
|
||||||
property int focusedIndex: -1
|
|
||||||
|
|
||||||
property int adaptiveCardSize: App.config.gridSize
|
|
||||||
property bool isTouchDevice: false
|
|
||||||
|
|
||||||
signal gameSelected(var game)
|
signal gameSelected(var game)
|
||||||
|
signal gameFocused(var game)
|
||||||
signal gameLaunched(var game)
|
signal gameLaunched(var game)
|
||||||
|
|
||||||
readonly property int gameCount: proxyModel.count
|
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() {
|
function restoreFocus() {
|
||||||
let w = applicationWindow()
|
grid.forceActiveFocus()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function launchFocusedGame() {
|
function focusedGame() {
|
||||||
if (!gameGrid || !proxyModel) return
|
if (grid.currentIndex < 0) return null
|
||||||
if (gameGrid.currentIndex < 0 && proxyModel.count > 0) {
|
return proxyModel.get(grid.currentIndex)
|
||||||
gameGrid.currentIndex = 0
|
|
||||||
libraryRoot.focusedIndex = 0
|
|
||||||
}
|
|
||||||
let game = proxyModel.get(gameGrid.currentIndex)
|
|
||||||
if (game) {
|
|
||||||
libraryRoot.gameLaunched(game)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDetailsForFocusedGame() {
|
function selectFocused() {
|
||||||
if (!gameGrid || !proxyModel) return
|
let g = focusedGame()
|
||||||
if (gameGrid.currentIndex < 0 && proxyModel.count > 0) {
|
if (g) libraryRoot.gameSelected(g)
|
||||||
gameGrid.currentIndex = 0
|
|
||||||
libraryRoot.focusedIndex = 0
|
|
||||||
}
|
|
||||||
let game = proxyModel.get(gameGrid.currentIndex)
|
|
||||||
if (game) {
|
|
||||||
libraryRoot.gameSelected(game)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearchActiveChanged: {
|
function launchFocused() {
|
||||||
if (!libraryRoot.searchActive) {
|
let g = focusedGame()
|
||||||
libraryRoot.clearSearch()
|
if (g) libraryRoot.gameLaunched(g)
|
||||||
Qt.callLater(function() {
|
|
||||||
gameGrid.forceActiveFocus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.fill: parent
|
||||||
anchors.margins: 0
|
focus: true
|
||||||
|
|
||||||
Item {
|
model: proxyModel
|
||||||
anchors.fill: parent
|
itemSize: Math.max(160, Math.min(App.config.gridSize, 280))
|
||||||
visible: libraryRoot.gameCount > 0
|
itemHeight: Math.round(grid.itemSize * 1.5)
|
||||||
|
adaptContent: true
|
||||||
|
enableLassoSelection: false
|
||||||
|
|
||||||
Image {
|
topPadding: 16
|
||||||
id: backgroundCoverA
|
bottomPadding: 24
|
||||||
anchors.fill: parent
|
leftPadding: 24
|
||||||
source: ""
|
rightPadding: 24
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
asynchronous: true
|
|
||||||
visible: source.toString().length > 0
|
|
||||||
smooth: true
|
|
||||||
mipmap: App.config.highQualityImages
|
|
||||||
opacity: 0.0
|
|
||||||
|
|
||||||
Behavior on opacity {
|
holder.visible: proxyModel.count === 0 && !App.importing
|
||||||
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic }
|
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
|
onCurrentIndexChanged: {
|
||||||
layer.effect: MultiEffect {
|
let g = proxyModel.get(currentIndex)
|
||||||
blurEnabled: true
|
if (g) libraryRoot.gameFocused(g)
|
||||||
blur: 0.9
|
}
|
||||||
blurMax: 64
|
|
||||||
}
|
delegate: GameCard {
|
||||||
|
game: model.gameObject
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
grid.currentIndex = index
|
||||||
|
libraryRoot.gameSelected(model.gameObject)
|
||||||
}
|
}
|
||||||
|
onPlayClicked: {
|
||||||
Image {
|
grid.currentIndex = index
|
||||||
id: backgroundCoverB
|
libraryRoot.gameLaunched(model.gameObject)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
Keys.onReturnPressed: libraryRoot.selectFocused()
|
||||||
anchors.fill: parent
|
Keys.onEnterPressed: libraryRoot.selectFocused()
|
||||||
spacing: Kirigami.Units.smallSpacing
|
Keys.onSpacePressed: libraryRoot.selectFocused()
|
||||||
|
|
||||||
SearchHeader {
|
QQC2.BusyIndicator {
|
||||||
id: searchHeader
|
anchors.centerIn: parent
|
||||||
Layout.fillWidth: true
|
running: App.importing
|
||||||
visible: libraryRoot.searchActive
|
visible: App.importing
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GameGridView {
|
Connections {
|
||||||
id: gameGrid
|
target: GamepadManager
|
||||||
Layout.fillWidth: true
|
function onSelectPressed() {
|
||||||
Layout.fillHeight: true
|
if (!grid.activeFocus) return
|
||||||
|
libraryRoot.selectFocused()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
function onNavigateUp() {
|
||||||
model: GameSortFilterModel {
|
if (!grid.activeFocus) return
|
||||||
id: proxyModel
|
if (grid.currentIndex <= 0) return
|
||||||
sourceModel: App.gameModel
|
let cols = Math.max(1, Math.floor(grid.width / grid.cellWidth))
|
||||||
showHidden: libraryRoot.filterSource === "hidden"
|
grid.currentIndex = Math.max(0, grid.currentIndex - cols)
|
||||||
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 onNavigateDown() {
|
||||||
Keys.onPressed: function(event) {
|
if (!grid.activeFocus) return
|
||||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
let cols = Math.max(1, Math.floor(grid.width / grid.cellWidth))
|
||||||
if (currentIndex < 0 && proxyModel.count > 0) {
|
grid.currentIndex = Math.min(proxyModel.count - 1, grid.currentIndex + cols)
|
||||||
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 onNavigateLeft() {
|
||||||
EmptyState {
|
if (!grid.activeFocus) return
|
||||||
anchors.centerIn: parent
|
grid.currentIndex = Math.max(0, grid.currentIndex - 1)
|
||||||
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 onNavigateRight() {
|
||||||
QQC2.BusyIndicator {
|
if (!grid.activeFocus) return
|
||||||
anchors.centerIn: parent
|
grid.currentIndex = Math.min(proxyModel.count - 1, grid.currentIndex + 1)
|
||||||
running: App.importing
|
|
||||||
visible: App.importing
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2337
src/qml/Main.qml
2337
src/qml/Main.qml
File diff suppressed because it is too large
Load diff
1
third-party/mauikit
vendored
Submodule
1
third-party/mauikit
vendored
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 32dbb346861fa89a1df9dfb82e54ff952754b232
|
||||||
1
third-party/mauiman
vendored
Submodule
1
third-party/mauiman
vendored
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 6ad438d97e000158aa0eda13ab669c208e804b0a
|
||||||
Loading…
Reference in a new issue