2026-01-18 12:13:07 +00:00
// 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
2026-01-24 12:27:30 +00:00
import QtQuick . Effects
2026-01-18 12:13:07 +00:00
import org . kde . kirigami as Kirigami
import org . kde . alakarte
import "components"
FocusScope {
id: libraryRoot
property string filterSource: "all"
property bool searchActive: false
2026-01-24 19:50:20 +00:00
property int focusedIndex: - 1
2026-01-18 12:13:07 +00:00
property int adaptiveCardSize: App . config . gridSize
property bool isTouchDevice: false
signal gameSelected ( var game )
signal gameLaunched ( var game )
2026-01-24 12:27:30 +00:00
readonly property int gameCount: proxyModel . count
property url focusedCoverUrl: ""
2026-01-18 12:13:07 +00:00
function focusSearch ( ) {
searchField . forceActiveFocus ( )
}
2026-01-24 12:27:30 +00:00
function clearSearch ( ) {
searchField . text = ""
proxyModel . filterText = ""
}
2026-01-18 12:13:07 +00:00
function restoreFocus ( ) {
if ( libraryRoot . searchActive ) {
libraryRoot . focusSearch ( )
} else {
2026-01-24 12:27:30 +00:00
if ( libraryRoot . focusedIndex >= 0 && libraryRoot . focusedIndex < proxyModel . count ) {
gameGrid . currentIndex = libraryRoot . focusedIndex
}
2026-01-18 12:13:07 +00:00
gameGrid . forceActiveFocus ( )
}
}
2026-01-24 12:27:30 +00:00
function launchFocusedGame ( ) {
if ( ! gameGrid || ! proxyModel ) return
2026-01-24 19:50:20 +00:00
if ( gameGrid . currentIndex < 0 && proxyModel . count > 0 ) {
gameGrid . currentIndex = 0
libraryRoot . focusedIndex = 0
}
2026-01-24 12:27:30 +00:00
let game = proxyModel . get ( gameGrid . currentIndex )
if ( game ) {
libraryRoot . gameLaunched ( game )
}
}
function openDetailsForFocusedGame ( ) {
if ( ! gameGrid || ! proxyModel ) return
2026-01-24 19:50:20 +00:00
if ( gameGrid . currentIndex < 0 && proxyModel . count > 0 ) {
gameGrid . currentIndex = 0
libraryRoot . focusedIndex = 0
}
2026-01-24 12:27:30 +00:00
let game = proxyModel . get ( gameGrid . currentIndex )
if ( game ) {
libraryRoot . gameSelected ( game )
}
}
2026-01-18 12:13:07 +00:00
onSearchActiveChanged: {
if ( ! libraryRoot . searchActive ) {
2026-01-24 12:27:30 +00:00
libraryRoot . clearSearch ( )
2026-01-18 12:13:07 +00:00
Qt . callLater ( function ( ) {
gameGrid . forceActiveFocus ( )
} )
}
}
Item {
anchors.fill: parent
anchors.margins: 0
2026-01-24 12:27:30 +00:00
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 )
}
}
2026-01-18 12:13:07 +00:00
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
2026-01-24 12:27:30 +00:00
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
}
2026-01-18 12:13:07 +00:00
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 ( ) {
2026-01-24 12:27:30 +00:00
gameGrid . currentIndex = index
libraryRoot . focusedIndex = index
2026-01-18 12:13:07 +00:00
card . clicked ( )
}
2026-01-24 12:27:30 +00:00
function play ( ) {
gameGrid . currentIndex = index
libraryRoot . focusedIndex = index
card . playClicked ( )
}
2026-01-18 12:13:07 +00:00
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 ) {
2026-01-24 19:50:20 +00:00
if ( currentIndex < 0 && proxyModel . count > 0 ) {
currentIndex = 0
libraryRoot . focusedIndex = 0
}
2026-01-18 12:13:07 +00:00
let game = proxyModel . get ( currentIndex )
if ( game ) {
libraryRoot . gameSelected ( game )
}
event . accepted = true
} else if ( event . key === Qt . Key_Space ) {
2026-01-24 19:50:20 +00:00
if ( currentIndex < 0 && proxyModel . count > 0 ) {
currentIndex = 0
libraryRoot . focusedIndex = 0
}
2026-01-18 12:13:07 +00:00
let game = proxyModel . get ( currentIndex )
if ( game ) {
libraryRoot . gameLaunched ( game )
}
event . accepted = true
}
}
EmptyState {
anchors.centerIn: parent
visible: proxyModel . count === 0 && ! App . importing
2026-01-24 12:27:30 +00:00
icon: proxyModel . filterText . length > 0 ? "edit-find" : ( libraryRoot . filterSource === "favorites" ? "bookmark-new" : ( libraryRoot . filterSource === "hidden" ? "view-hidden" : "applications-games" ) )
2026-01-18 12:13:07 +00:00
title: proxyModel . filterText . length > 0 ?
2026-01-24 12:27:30 +00:00
i18n ( "No games found" ) : ( libraryRoot . filterSource === "favorites" ? i18n ( "No favorites yet" ) : ( libraryRoot . filterSource === "hidden" ? i18n ( "No hidden games" ) : i18n ( "Your library is empty" ) ) )
2026-01-18 12:13:07 +00:00
description: proxyModel . filterText . length > 0 ?
2026-01-24 12:27:30 +00:00
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" ) ) )
2026-01-18 12:13:07 +00:00
2026-01-24 12:27:30 +00:00
actionText: ( proxyModel . filterText . length > 0 || libraryRoot . filterSource === "favorites" || libraryRoot . filterSource === "hidden" ) ? "" : i18n ( "Import Games" )
2026-01-18 12:13:07 +00:00
onActionTriggered: App . importAllGames ( )
}
QQC2 . BusyIndicator {
anchors.centerIn: parent
running: App . importing
visible: App . importing
}
}
}
}
}