Fix console UI regressions: icons, hint bar, category rail, dialog

- ConsoleCategoryRail: port from CouchSidebar using Kirigami.Icon +
  IconWithResourceFallback, radius: smallSpacing (not pill), Hidden
  source, Import+Settings buttons with tooltips, App.gameModel.gameAt()
- Main.qml: use BottomHintBar component, wire sourceSelected/
  settingsRequested/importRequested signals, flat ToolButton for search,
  declare GameEditDialog as proper child, remove Maui.ContextualMenu
- GameCard: Maui.Icon -> Kirigami.Icon, Maui.Theme -> Kirigami.Theme
- ConsoleGameDetail: Maui.Icon -> Kirigami.Icon, Maui.Theme ->
  Kirigami.Theme, replace broken inline hint rows with BottomHintBar
This commit is contained in:
Marco Allegretti 2026-03-23 10:31:26 +01:00
parent 84c6795fd6
commit cb69f801bf
4 changed files with 204 additions and 264 deletions

View file

@ -4,73 +4,96 @@
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.mauikit.controls as Maui
import org.kde.kirigami as Kirigami
import org.kde.alakarte
import "components"
Item {
id: root
property string currentCategory: "all"
property string currentSource: "all"
signal categorySelected(string categoryId)
signal sourceSelected(string source)
signal settingsRequested()
signal importRequested()
function selectNext() {
if (rail.currentIndex < categoryModel.count - 1) {
rail.currentIndex++
categorySelected(categoryModel.get(rail.currentIndex).categoryId)
}
if (sourceModel.count <= 0) return
let i = (tabList.currentIndex + 1) % sourceModel.count
_applyIndex(i)
}
function selectPrevious() {
if (rail.currentIndex > 0) {
rail.currentIndex--
categorySelected(categoryModel.get(rail.currentIndex).categoryId)
if (sourceModel.count <= 0) return
let i = tabList.currentIndex - 1
if (i < 0) i = sourceModel.count - 1
_applyIndex(i)
}
function _applyIndex(i) {
tabList.currentIndex = i
let item = sourceModel.get(i)
if (item) {
root.currentSource = item.sourceId
root.sourceSelected(item.sourceId)
tabList.positionViewAtIndex(i, ListView.Contain)
}
}
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"
function iconInfoForPlatform(platformName) {
let p = (platformName || "").toLowerCase()
if (p.includes("steam")) return { source: "com.valvesoftware.Steam", fallback: "steam", resourceFallback: Qt.resolvedUrl("icons/brand/steam-symbolic.svg") }
if (p.includes("itch")) return { source: "io.itch.itch", fallback: "applications-games", resourceFallback: Qt.resolvedUrl("icons/brand/itchdotio-symbolic.svg") }
if (p.includes("retroarch")) return { source: "org.libretro.RetroArch", fallback: "applications-games", resourceFallback: Qt.resolvedUrl("icons/brand/retroarch-symbolic.svg") }
if (p.includes("lutris")) return { source: "lutris", fallback: "applications-games", resourceFallback: "" }
if (p.includes("heroic")) return { source: "com.heroicgameslauncher.hgl", fallback: "applications-games", resourceFallback: "" }
if (p.includes("bottles")) return { source: "com.usebottles.bottles", fallback: "application-x-executable", resourceFallback: "" }
if (p.includes("flatpak")) return { source: "flatpak-discover", fallback: "applications-games", resourceFallback: "" }
if (p.includes("desktop")) return { source: "user-desktop", fallback: "computer", resourceFallback: "" }
if (p.includes("legendary")) return { source: "legendary", fallback: "applications-games", resourceFallback: "" }
return { source: "applications-games", fallback: "applications-games", resourceFallback: "" }
}
GameSortFilterModel {
id: allGames
sourceModel: App.gameModel
showHidden: false
onCurrentSourceChanged: {
for (let i = 0; i < sourceModel.count; ++i) {
if (sourceModel.get(i).sourceId === currentSource) {
tabList.currentIndex = i
return
}
}
}
ListModel {
id: categoryModel
id: sourceModel
Component.onCompleted: refresh()
Component.onCompleted: rebuild()
function rebuild() {
function refresh() {
clear()
append({ label: i18n("All"), categoryId: "all", iconSource: "view-list-icons" })
append({ label: i18n("Favorites"), categoryId: "favorites", iconSource: "starred-symbolic" })
let allCount = 0, favCount = 0, hiddenCount = 0
let sources = {}
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 i = 0; i < App.gameModel.rowCount(); i++) {
let game = App.gameModel.gameAt(i)
if (!game) continue
if (game.hidden) { hiddenCount++; continue }
allCount++
if (game.favorite) favCount++
let p = game.platform
sources[p] = (sources[p] || 0) + 1
}
append({ name: i18n("All"), sourceId: "all", icon: "view-list-icons", fallback: "applications-games", resourceFallback: "", count: allCount })
append({ name: i18n("Favorites"), sourceId: "favorites", icon: "bookmark-new", fallback: "bookmark-new", resourceFallback: "", count: favCount })
append({ name: i18n("Hidden"), sourceId: "hidden", icon: "view-hidden", fallback: "view-hidden", resourceFallback: "", count: hiddenCount })
for (let platform in sources) {
let info = root.iconInfoForPlatform(platform)
append({ name: platform, sourceId: platform, icon: info.source, fallback: info.fallback, resourceFallback: info.resourceFallback, count: sources[platform] })
}
for (let j = 0; j < count; ++j) {
if (get(j).categoryId === root.currentCategory) {
rail.currentIndex = j
if (get(j).sourceId === root.currentSource) {
tabList.currentIndex = j
break
}
}
@ -79,83 +102,112 @@ Item {
Connections {
target: App.gameModel
function onCountChanged() { categoryModel.rebuild() }
function onCountChanged() { sourceModel.refresh() }
}
Connections {
target: App
function onImportCompleted() { sourceModel.refresh() }
}
onCurrentCategoryChanged: {
for (let j = 0; j < categoryModel.count; ++j) {
if (categoryModel.get(j).categoryId === currentCategory) {
rail.currentIndex = j
return
}
}
}
Rectangle {
RowLayout {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.55)
spacing: 0
QQC2.ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
QQC2.ScrollBar.vertical.policy: QQC2.ScrollBar.AlwaysOff
ListView {
id: rail
anchors {
fill: parent
leftMargin: 24
rightMargin: 24
}
id: tabList
model: sourceModel
orientation: ListView.Horizontal
spacing: 8
spacing: Kirigami.Units.smallSpacing
leftMargin: Kirigami.Units.largeSpacing
rightMargin: Kirigami.Units.largeSpacing
topMargin: Kirigami.Units.smallSpacing
bottomMargin: Kirigami.Units.smallSpacing
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
id: tabDelegate
width: implicitWidth
height: tabList.height - tabList.topMargin - tabList.bottomMargin
readonly property bool isCurrent: ListView.isCurrentItem
readonly property bool isActive: model.sourceId === root.currentSource
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
radius: Kirigami.Units.smallSpacing
color: tabDelegate.isActive ? Kirigami.Theme.highlightColor
: (tabDelegate.hovered ? Kirigami.Theme.alternateBackgroundColor : "transparent")
opacity: tabDelegate.isActive ? 0.22 : (tabDelegate.hovered ? 0.12 : 0.0)
Behavior on color { ColorAnimation { duration: 120 } }
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 2
radius: 1
color: Kirigami.Theme.highlightColor
visible: tabDelegate.isActive
}
contentItem: RowLayout {
spacing: 6
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } }
}
Maui.Icon {
source: model.iconSource
Layout.preferredWidth: 16
Layout.preferredHeight: 16
color: "white"
opacity: pill.isCurrent ? 1.0 : 0.75
leftPadding: Kirigami.Units.mediumSpacing
rightPadding: Kirigami.Units.mediumSpacing
contentItem: RowLayout {
spacing: Kirigami.Units.smallSpacing
IconWithResourceFallback {
primary: model.icon
secondary: model.fallback
resourceFallback: model.resourceFallback ? Qt.resolvedUrl(model.resourceFallback) : ""
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
color: tabDelegate.isActive ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor
}
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
text: model.name
color: tabDelegate.isActive ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor
font.bold: tabDelegate.isActive
elide: Text.ElideRight
maximumLineCount: 1
}
}
onClicked: {
rail.currentIndex = index
root.categorySelected(model.categoryId)
root.currentSource = model.sourceId
root.sourceSelected(model.sourceId)
}
}
}
}
Kirigami.Separator { Layout.fillHeight: true }
QQC2.ToolButton {
icon.name: "document-import"
onClicked: root.importRequested()
Layout.fillHeight: true
flat: true
QQC2.ToolTip.text: i18n("Import Games")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
icon.name: "configure"
onClicked: root.settingsRequested()
Layout.fillHeight: true
flat: true
QQC2.ToolTip.text: i18n("Settings")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
}

View file

@ -5,8 +5,9 @@ import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtQuick.Effects
import org.mauikit.controls as Maui
import org.kde.kirigami as Kirigami
import org.kde.alakarte
import "components"
Item {
id: detailRoot
@ -202,7 +203,7 @@ Item {
contentItem: RowLayout {
spacing: 8
Maui.Icon {
Kirigami.Icon {
source: "media-playback-start"
width: 22; height: 22
color: "white"
@ -218,9 +219,9 @@ Item {
background: Rectangle {
radius: 8
color: playBtn.activeFocus
? Maui.Theme.highlightColor
? Kirigami.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.color: playBtn.activeFocus ? Kirigami.Theme.highlightColor : Qt.rgba(1,1,1,0.28)
border.width: playBtn.activeFocus ? 0 : 1
Behavior on color { ColorAnimation { duration: 100 } }
}
@ -239,7 +240,7 @@ Item {
KeyNavigation.left: playBtn
KeyNavigation.right: editBtn
contentItem: Maui.Icon {
contentItem: Kirigami.Icon {
source: detailRoot.game && detailRoot.game.favorite ? "starred-symbolic" : "non-starred-symbolic"
width: 22; height: 22
color: detailRoot.game && detailRoot.game.favorite ? "#f5c518" : "white"
@ -271,7 +272,7 @@ Item {
KeyNavigation.left: favoriteBtn
KeyNavigation.right: closeBtn
contentItem: Maui.Icon {
contentItem: Kirigami.Icon {
source: "document-edit"
width: 20; height: 20
color: "white"
@ -301,7 +302,7 @@ Item {
icon.name: "window-close"
KeyNavigation.left: editBtn
contentItem: Maui.Icon {
contentItem: Kirigami.Icon {
source: "window-close"
width: 20; height: 20
color: "white"
@ -327,36 +328,11 @@ Item {
}
}
Item { Layout.preferredHeight: 32 }
Item { Layout.preferredHeight: 16 }
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
}
}
}
BottomHintBar {
uiMode: Config.Couch
context: "details"
}
Item { Layout.fillHeight: true; Layout.preferredHeight: 1 }

View file

@ -5,7 +5,7 @@ import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtQuick.Effects
import org.mauikit.controls as Maui
import org.kde.kirigami as Kirigami
import org.kde.alakarte
Item {
@ -41,9 +41,9 @@ Item {
layer.effect: MultiEffect {
shadowEnabled: true
shadowColor: Qt.rgba(
Maui.Theme.highlightColor.r,
Maui.Theme.highlightColor.g,
Maui.Theme.highlightColor.b, 0.55)
Kirigami.Theme.highlightColor.r,
Kirigami.Theme.highlightColor.g,
Kirigami.Theme.highlightColor.b, 0.55)
shadowBlur: 0.8
shadowHorizontalOffset: 0
shadowVerticalOffset: 6
@ -78,7 +78,7 @@ Item {
smooth: true
}
Maui.Icon {
Kirigami.Icon {
anchors.centerIn: parent
source: "applications-games"
width: parent.width * 0.38
@ -133,7 +133,7 @@ Item {
}
}
Maui.Icon {
Kirigami.Icon {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: 8
@ -149,7 +149,7 @@ Item {
anchors.fill: coverFrame
radius: 8
color: "transparent"
border.color: Maui.Theme.highlightColor
border.color: Kirigami.Theme.highlightColor
border.width: gameCard.isCurrent ? 3 : 0
opacity: gameCard.isCurrent ? 1.0 : 0.0
Behavior on opacity { NumberAnimation { duration: 120 } }

View file

@ -7,6 +7,7 @@ import QtQuick.Layouts
import QtQuick.Effects
import org.mauikit.controls as Maui
import org.kde.alakarte
import "components"
Maui.ApplicationWindow {
id: root
@ -21,7 +22,6 @@ Maui.ApplicationWindow {
property bool detailVisible: false
property bool searchVisible: false
property bool settingsVisible: false
property bool importVisible: false
property string searchText: ""
color: "#0d0d14"
@ -99,8 +99,8 @@ Maui.ApplicationWindow {
RowLayout {
anchors.fill: parent
anchors.leftMargin: 20
anchors.rightMargin: 12
anchors.leftMargin: 12
anchors.rightMargin: 0
Image {
source: "qrc:/icons/sc-apps-org.kde.alakarte.svg"
@ -110,55 +110,26 @@ Maui.ApplicationWindow {
smooth: true
}
Item { Layout.preferredWidth: 8 }
Item { Layout.preferredWidth: 4 }
ConsoleCategoryRail {
id: categoryRail
Layout.fillWidth: true
Layout.fillHeight: true
currentCategory: root.currentCategory
onCategorySelected: function(cat) {
root.currentCategory = cat
}
currentSource: root.currentCategory
onSourceSelected: function(src) { root.currentCategory = src }
onSettingsRequested: root.settingsVisible = true
onImportRequested: App.importAllGames()
}
QQC2.ToolButton {
id: searchBtn
icon.name: "edit-find"
icon.color: "white"
flat: true
Layout.preferredWidth: 40
Layout.preferredHeight: 40
onClicked: root.searchVisible = !root.searchVisible
background: Rectangle {
radius: 6
color: searchBtn.pressed ? Qt.rgba(1,1,1,0.18) : (searchBtn.hovered ? Qt.rgba(1,1,1,0.12) : "transparent")
}
}
QQC2.ToolButton {
id: settingsBtn
icon.name: "configure"
icon.color: "white"
Layout.preferredWidth: 40
Layout.preferredHeight: 40
onClicked: root.settingsVisible = true
background: Rectangle {
radius: 6
color: settingsBtn.pressed ? Qt.rgba(1,1,1,0.18) : (settingsBtn.hovered ? Qt.rgba(1,1,1,0.12) : "transparent")
}
}
QQC2.ToolButton {
id: importBtn
icon.name: "document-import"
icon.color: "white"
Layout.preferredWidth: 40
Layout.preferredHeight: 40
onClicked: root.importVisible = true
background: Rectangle {
radius: 6
color: importBtn.pressed ? Qt.rgba(1,1,1,0.18) : (importBtn.hovered ? Qt.rgba(1,1,1,0.12) : "transparent")
}
QQC2.ToolTip.text: i18n("Search")
QQC2.ToolTip.visible: hovered
}
}
}
@ -172,14 +143,6 @@ Maui.ApplicationWindow {
Layout.topMargin: 4
visible: root.searchVisible
placeholderText: i18n("Search games…")
color: "white"
font.pixelSize: 15
background: Rectangle {
radius: 8
color: Qt.rgba(1, 1, 1, 0.10)
border.color: Qt.rgba(1, 1, 1, 0.22)
border.width: 1
}
onTextChanged: root.searchText = text
Keys.onEscapePressed: {
root.searchVisible = false
@ -196,54 +159,24 @@ Maui.ApplicationWindow {
filterSource: root.currentCategory
filterText: root.searchText
onGameFocused: function(game) {
root.focusedGame = game
}
onGameFocused: function(game) { root.focusedGame = game }
onGameSelected: function(game) {
root.selectedGame = game
root.detailVisible = true
}
onGameLaunched: function(game) {
App.launcher.launchGame(game)
}
onGameLaunched: function(game) { App.launcher.launchGame(game) }
}
Rectangle {
Layout.fillWidth: true
height: 38
color: Qt.rgba(0, 0, 0, 0.55)
visible: GamepadManager.connected
implicitHeight: hintBar.implicitHeight + 12
RowLayout {
BottomHintBar {
id: hintBar
anchors.centerIn: parent
spacing: 32
Repeater {
model: [
{ icon: ":/icons/gamepad/generic/south.svg", label: i18n("Select / Details") },
{ icon: ":/icons/gamepad/generic/east.svg", label: i18n("Back") },
{ icon: ":/icons/gamepad/generic/lb.svg", label: i18n("Prev Category") },
{ icon: ":/icons/gamepad/generic/rb.svg", label: i18n("Next Category") },
{ icon: ":/icons/gamepad/generic/menu.svg", label: i18n("Settings") }
]
RowLayout {
spacing: 5
Image {
source: modelData.icon
width: 18; height: 18
fillMode: Image.PreserveAspectFit
smooth: true
}
QQC2.Label {
text: modelData.label
color: Qt.rgba(1, 1, 1, 0.6)
font.pixelSize: 12
}
}
}
uiMode: Config.Couch
context: root.detailVisible ? "details" : "library"
}
}
}
@ -299,20 +232,8 @@ Maui.ApplicationWindow {
onLoaded: item.forceActiveFocus()
}
Loader {
id: importLoader
anchors.fill: parent
z: 20
active: root.importVisible
sourceComponent: DiagnosticsSheet {
onClosed: root.importVisible = false
}
onLoaded: item.forceActiveFocus()
}
Maui.ContextualMenu {
GameEditDialog {
id: gameEditDialog
property var game: null
}
Connections {
@ -320,13 +241,11 @@ Maui.ApplicationWindow {
function onBackPressed() {
if (root.settingsVisible) { root.settingsVisible = false; return }
if (root.importVisible) { root.importVisible = false; return }
if (root.searchVisible) {
root.searchVisible = false
root.searchText = ""
searchField.text = ""
libraryView.restoreFocus()
return
}
}
@ -336,9 +255,7 @@ Maui.ApplicationWindow {
function onSearchPressed() {
root.searchVisible = !root.searchVisible
if (root.searchVisible) {
Qt.callLater(function() { searchField.forceActiveFocus() })
}
if (root.searchVisible) Qt.callLater(function() { searchField.forceActiveFocus() })
}
function onLeftBumperPressed() {
@ -349,9 +266,4 @@ Maui.ApplicationWindow {
if (!root.detailVisible) categoryRail.selectNext()
}
}
Connections {
target: App
function onImportingChanged() {}
}
}