a-la-karte/src/qml/LibraryView.qml
Marco Allegretti 24b9ee6491 UI: Add hint bar and UI mode override
Add a bottom hint bar with keyboard/gamepad hints and controller-specific icons.

Also add a UI mode setting (auto/desktop/handheld) to improve the adaptive layout on different form factors.
2026-01-24 13:27:30 +01:00

288 lines
9.8 KiB
QML

// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024 A-La-Karte Contributors
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtQuick.Effects
import org.kde.kirigami as Kirigami
import org.kde.alakarte
import "components"
FocusScope {
id: libraryRoot
property string filterSource: "all"
property bool searchActive: false
property int focusedIndex: 0
property int adaptiveCardSize: App.config.gridSize
property bool isTouchDevice: false
signal gameSelected(var game)
signal gameLaunched(var game)
readonly property int gameCount: proxyModel.count
property url focusedCoverUrl: ""
function focusSearch() {
searchField.forceActiveFocus()
}
function clearSearch() {
searchField.text = ""
proxyModel.filterText = ""
}
function restoreFocus() {
if (libraryRoot.searchActive) {
libraryRoot.focusSearch()
} else {
if (libraryRoot.focusedIndex >= 0 && libraryRoot.focusedIndex < proxyModel.count) {
gameGrid.currentIndex = libraryRoot.focusedIndex
}
gameGrid.forceActiveFocus()
}
}
function launchFocusedGame() {
if (!gameGrid || !proxyModel) return
let game = proxyModel.get(gameGrid.currentIndex)
if (game) {
libraryRoot.gameLaunched(game)
}
}
function openDetailsForFocusedGame() {
if (!gameGrid || !proxyModel) return
let game = proxyModel.get(gameGrid.currentIndex)
if (game) {
libraryRoot.gameSelected(game)
}
}
onSearchActiveChanged: {
if (!libraryRoot.searchActive) {
libraryRoot.clearSearch()
Qt.callLater(function() {
gameGrid.forceActiveFocus()
})
}
}
Item {
anchors.fill: parent
anchors.margins: 0
Item {
anchors.fill: parent
visible: libraryRoot.gameCount > 0
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
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
}
}
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)
}
}
ColumnLayout {
anchors.fill: parent
spacing: Kirigami.Units.smallSpacing
SearchHeader {
id: searchHeader
Layout.fillWidth: true
visible: libraryRoot.searchActive
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()
}
}
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
}
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)
}
}
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
let game = proxyModel.get(currentIndex)
if (game) {
libraryRoot.gameSelected(game)
}
event.accepted = true
} else if (event.key === Qt.Key_Space) {
let game = proxyModel.get(currentIndex)
if (game) {
libraryRoot.gameLaunched(game)
}
event.accepted = true
}
}
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()
}
QQC2.BusyIndicator {
anchors.centerIn: parent
running: App.importing
visible: App.importing
}
}
}
}
}