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
|
||||
)
|
||||
|
||||
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
|
||||
"$<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/krunner)
|
||||
add_subdirectory(icons)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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-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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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