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.
This commit is contained in:
Marco Allegretti 2026-01-24 13:27:30 +01:00
parent 24c0137fff
commit 24b9ee6491
49 changed files with 1140 additions and 74 deletions

View file

@ -69,11 +69,48 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte
qml/components/SearchHeader.qml qml/components/SearchHeader.qml
qml/components/IconWithResourceFallback.qml qml/components/IconWithResourceFallback.qml
qml/components/EmptyState.qml qml/components/EmptyState.qml
qml/components/BottomHintBar.qml
RESOURCES RESOURCES
qml/icons/brand/steam-symbolic.svg qml/icons/brand/steam-symbolic.svg
qml/icons/brand/itchdotio-symbolic.svg qml/icons/brand/itchdotio-symbolic.svg
qml/icons/brand/retroarch-symbolic.svg qml/icons/brand/retroarch-symbolic.svg
qml/icons/app/org.kde.alakarte.svg qml/icons/app/org.kde.alakarte.svg
qml/icons/gamepad/playstation/cross.svg
qml/icons/gamepad/playstation/circle.svg
qml/icons/gamepad/playstation/square.svg
qml/icons/gamepad/playstation/triangle.svg
qml/icons/gamepad/playstation/options.svg
qml/icons/gamepad/playstation/share.svg
qml/icons/gamepad/xbox/a.svg
qml/icons/gamepad/xbox/b.svg
qml/icons/gamepad/xbox/x.svg
qml/icons/gamepad/xbox/y.svg
qml/icons/gamepad/xbox/menu.svg
qml/icons/gamepad/xbox/view.svg
qml/icons/gamepad/nintendo/a.svg
qml/icons/gamepad/nintendo/b.svg
qml/icons/gamepad/nintendo/x.svg
qml/icons/gamepad/nintendo/y.svg
qml/icons/gamepad/nintendo/plus.svg
qml/icons/gamepad/nintendo/minus.svg
qml/icons/gamepad/nintendo/home.svg
qml/icons/gamepad/generic/south.svg
qml/icons/gamepad/generic/east.svg
qml/icons/gamepad/generic/west.svg
qml/icons/gamepad/generic/north.svg
qml/icons/gamepad/generic/lb.svg
qml/icons/gamepad/generic/rb.svg
qml/icons/gamepad/generic/lt.svg
qml/icons/gamepad/generic/rt.svg
qml/icons/gamepad/generic/dpad.svg
qml/icons/gamepad/generic/dpad-up.svg
qml/icons/gamepad/generic/dpad-down.svg
qml/icons/gamepad/generic/dpad-left.svg
qml/icons/gamepad/generic/dpad-right.svg
qml/icons/gamepad/generic/lstick.svg
qml/icons/gamepad/generic/rstick.svg
qml/icons/gamepad/generic/menu.svg
qml/icons/gamepad/generic/back.svg
SOURCES SOURCES
app.h app.h
gamemodel.h gamemodel.h

View file

@ -26,6 +26,20 @@ void Config::setViewMode(ViewMode mode)
} }
} }
Config::UiMode Config::uiMode() const
{
return m_uiMode;
}
void Config::setUiMode(UiMode mode)
{
if (m_uiMode != mode) {
m_uiMode = mode;
save();
Q_EMIT uiModeChanged();
}
}
int Config::gridSize() const int Config::gridSize() const
{ {
return m_gridSize; return m_gridSize;
@ -271,6 +285,7 @@ void Config::save()
KConfigGroup generalGroup(config, QStringLiteral("General")); KConfigGroup generalGroup(config, QStringLiteral("General"));
generalGroup.writeEntry(QStringLiteral("viewMode"), static_cast<int>(m_viewMode)); generalGroup.writeEntry(QStringLiteral("viewMode"), static_cast<int>(m_viewMode));
generalGroup.writeEntry(QStringLiteral("uiMode"), static_cast<int>(m_uiMode));
generalGroup.writeEntry(QStringLiteral("gridSize"), m_gridSize); generalGroup.writeEntry(QStringLiteral("gridSize"), m_gridSize);
generalGroup.writeEntry(QStringLiteral("showPlatformBadges"), m_showPlatformBadges); generalGroup.writeEntry(QStringLiteral("showPlatformBadges"), m_showPlatformBadges);
generalGroup.writeEntry(QStringLiteral("theme"), m_theme); generalGroup.writeEntry(QStringLiteral("theme"), m_theme);
@ -302,6 +317,7 @@ void Config::load()
const KConfigGroup generalGroup(config, QStringLiteral("General")); const KConfigGroup generalGroup(config, QStringLiteral("General"));
m_viewMode = static_cast<ViewMode>(generalGroup.readEntry(QStringLiteral("viewMode"), static_cast<int>(GridView))); m_viewMode = static_cast<ViewMode>(generalGroup.readEntry(QStringLiteral("viewMode"), static_cast<int>(GridView)));
m_uiMode = static_cast<UiMode>(generalGroup.readEntry(QStringLiteral("uiMode"), static_cast<int>(Auto)));
m_gridSize = generalGroup.readEntry(QStringLiteral("gridSize"), 180); m_gridSize = generalGroup.readEntry(QStringLiteral("gridSize"), 180);
m_showPlatformBadges = generalGroup.readEntry(QStringLiteral("showPlatformBadges"), true); m_showPlatformBadges = generalGroup.readEntry(QStringLiteral("showPlatformBadges"), true);
m_theme = generalGroup.readEntry(QStringLiteral("theme"), QString()); m_theme = generalGroup.readEntry(QStringLiteral("theme"), QString());
@ -328,6 +344,7 @@ void Config::load()
void Config::resetToDefaults() void Config::resetToDefaults()
{ {
m_viewMode = GridView; m_viewMode = GridView;
m_uiMode = Auto;
m_gridSize = 180; m_gridSize = 180;
m_showPlatformBadges = true; m_showPlatformBadges = true;
m_autoImportOnStartup = false; m_autoImportOnStartup = false;
@ -349,6 +366,7 @@ void Config::resetToDefaults()
save(); save();
Q_EMIT viewModeChanged(); Q_EMIT viewModeChanged();
Q_EMIT uiModeChanged();
Q_EMIT gridSizeChanged(); Q_EMIT gridSizeChanged();
Q_EMIT showPlatformBadgesChanged(); Q_EMIT showPlatformBadgesChanged();
Q_EMIT autoImportOnStartupChanged(); Q_EMIT autoImportOnStartupChanged();

View file

@ -12,6 +12,7 @@ class Config : public QObject
QML_ELEMENT QML_ELEMENT
Q_PROPERTY(ViewMode viewMode READ viewMode WRITE setViewMode NOTIFY viewModeChanged) Q_PROPERTY(ViewMode viewMode READ viewMode WRITE setViewMode NOTIFY viewModeChanged)
Q_PROPERTY(UiMode uiMode READ uiMode WRITE setUiMode NOTIFY uiModeChanged)
Q_PROPERTY(int gridSize READ gridSize WRITE setGridSize NOTIFY gridSizeChanged) Q_PROPERTY(int gridSize READ gridSize WRITE setGridSize NOTIFY gridSizeChanged)
Q_PROPERTY(bool showPlatformBadges READ showPlatformBadges WRITE setShowPlatformBadges NOTIFY showPlatformBadgesChanged) Q_PROPERTY(bool showPlatformBadges READ showPlatformBadges WRITE setShowPlatformBadges NOTIFY showPlatformBadgesChanged)
Q_PROPERTY(bool autoImportOnStartup READ autoImportOnStartup WRITE setAutoImportOnStartup NOTIFY autoImportOnStartupChanged) Q_PROPERTY(bool autoImportOnStartup READ autoImportOnStartup WRITE setAutoImportOnStartup NOTIFY autoImportOnStartupChanged)
@ -38,11 +39,21 @@ public:
}; };
Q_ENUM(ViewMode) Q_ENUM(ViewMode)
enum UiMode {
Auto,
Desktop,
Handheld
};
Q_ENUM(UiMode)
explicit Config(QObject *parent = nullptr); explicit Config(QObject *parent = nullptr);
ViewMode viewMode() const; ViewMode viewMode() const;
void setViewMode(ViewMode mode); void setViewMode(ViewMode mode);
UiMode uiMode() const;
void setUiMode(UiMode mode);
int gridSize() const; int gridSize() const;
void setGridSize(int size); void setGridSize(int size);
@ -100,6 +111,7 @@ public:
Q_SIGNALS: Q_SIGNALS:
void viewModeChanged(); void viewModeChanged();
void uiModeChanged();
void gridSizeChanged(); void gridSizeChanged();
void showPlatformBadgesChanged(); void showPlatformBadgesChanged();
void autoImportOnStartupChanged(); void autoImportOnStartupChanged();
@ -120,6 +132,7 @@ Q_SIGNALS:
private: private:
ViewMode m_viewMode = GridView; ViewMode m_viewMode = GridView;
UiMode m_uiMode = Auto;
int m_gridSize = 180; int m_gridSize = 180;
bool m_showPlatformBadges = true; bool m_showPlatformBadges = true;
bool m_autoImportOnStartup = false; bool m_autoImportOnStartup = false;

View file

@ -10,6 +10,22 @@ GameSortFilterModel::GameSortFilterModel(QObject *parent)
{ {
setDynamicSortFilter(true); setDynamicSortFilter(true);
sort(0); sort(0);
connect(this, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &, int, int) {
Q_EMIT countChanged();
});
connect(this, &QAbstractItemModel::rowsRemoved, this, [this](const QModelIndex &, int, int) {
Q_EMIT countChanged();
});
connect(this, &QAbstractItemModel::rowsMoved, this, [this](const QModelIndex &, int, int, const QModelIndex &, int) {
Q_EMIT countChanged();
});
connect(this, &QAbstractItemModel::modelReset, this, [this] {
Q_EMIT countChanged();
});
connect(this, &QAbstractItemModel::layoutChanged, this, [this](const QList<QPersistentModelIndex> &, QAbstractItemModel::LayoutChangeHint) {
Q_EMIT countChanged();
});
} }
QString GameSortFilterModel::filterText() const QString GameSortFilterModel::filterText() const
@ -169,6 +185,7 @@ bool GameSortFilterModel::lessThan(const QModelIndex &left, const QModelIndex &r
void GameSortFilterModel::invalidateAndEmit() void GameSortFilterModel::invalidateAndEmit()
{ {
invalidateFilter(); beginFilterChange();
endFilterChange();
Q_EMIT countChanged(); Q_EMIT countChanged();
} }

View file

@ -15,9 +15,14 @@ FocusScope {
property bool showPlayButton: true property bool showPlayButton: true
property bool focused: activeFocus property bool focused: activeFocus
readonly property bool isTouchDevice: Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile readonly property bool isTouchDevice: {
let w = applicationWindow()
if (w && w.isTouchDevice !== undefined) return w.isTouchDevice
return Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile
}
readonly property int adaptiveFocusRingWidth: 1 readonly property int adaptiveFocusRingWidth: 1
readonly property real adaptiveHoverScale: 1.015 readonly property real adaptiveHoverScale: 1.015
readonly property real adaptiveFocusScale: 1.03
readonly property bool useAnimatedCover: App.config.animatedCovers readonly property bool useAnimatedCover: App.config.animatedCovers
&& game && game
@ -38,24 +43,50 @@ FocusScope {
color: Kirigami.Theme.backgroundColor color: Kirigami.Theme.backgroundColor
shadow { shadow {
size: (hoverHandler.hovered || gameCard.focused) ? Kirigami.Units.mediumSpacing : Kirigami.Units.smallSpacing size: gameCard.focused ? Kirigami.Units.mediumSpacing : (hoverHandler.hovered ? Kirigami.Units.smallSpacing * 1.5 : Kirigami.Units.smallSpacing)
color: (hoverHandler.hovered || gameCard.focused) ? Qt.rgba(0, 0, 0, 0.30) : Qt.rgba(0, 0, 0, 0.16) color: gameCard.focused ? 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.width: gameCard.focused ? gameCard.adaptiveFocusRingWidth : 0
border.color: Kirigami.Theme.highlightColor border.color: Kirigami.Theme.highlightColor
Behavior on border.width { Behavior on border.width {
NumberAnimation { duration: Kirigami.Units.shortDuration } NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
} }
Behavior on shadow.size { Behavior on shadow.size {
NumberAnimation { duration: Kirigami.Units.shortDuration } NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
} }
scale: (hoverHandler.hovered || gameCard.focused) ? gameCard.adaptiveHoverScale : 1.0 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 } NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
}
Rectangle {
anchors.fill: parent
radius: Kirigami.Units.mediumSpacing
color: "transparent"
border.width: 2
border.color: Kirigami.Theme.highlightColor
opacity: 0.0
visible: gameCard.focused
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 }
}
} }
// Cover image // Cover image

View file

@ -13,10 +13,18 @@ Kirigami.OverlaySheet {
property var game property var game
readonly property bool isMobile: Kirigami.Settings.isMobile readonly property bool isMobile: {
let w = applicationWindow()
if (w && w.isMobile !== undefined) return w.isMobile
return Kirigami.Settings.isMobile
}
readonly property bool isNarrowScreen: applicationWindow() && applicationWindow().width < Kirigami.Units.gridUnit * 30 readonly property bool isNarrowScreen: applicationWindow() && applicationWindow().width < Kirigami.Units.gridUnit * 30
readonly property bool useCompactLayout: isMobile || isNarrowScreen readonly property bool useCompactLayout: isMobile || isNarrowScreen
readonly property bool isTouchDevice: Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile readonly property bool isTouchDevice: {
let w = applicationWindow()
if (w && w.isTouchDevice !== undefined) return w.isTouchDevice
return Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile
}
readonly property int actionIconButtonSize: isTouchDevice ? Math.round(Kirigami.Units.gridUnit * 2.5) : Math.round(Kirigami.Units.gridUnit * 2) readonly property int actionIconButtonSize: isTouchDevice ? Math.round(Kirigami.Units.gridUnit * 2.5) : Math.round(Kirigami.Units.gridUnit * 2)

View file

@ -67,6 +67,48 @@ Kirigami.Dialog {
property string selectedCoverPath: "" property string selectedCoverPath: ""
function isDescendant(item, ancestor) {
let p = item
while (p) {
if (p === ancestor || (ancestor.contentItem && p === ancestor.contentItem)) return true
p = p.parent
}
return false
}
function focusNextInChain(forward) {
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
let next = w.activeFocusItem
for (let i = 0; i < 50; i++) {
next = next.nextItemInFocusChain(forward)
if (!next) return
if (dialog.isDescendant(next, dialog)) {
next.forceActiveFocus()
return
}
}
}
function activateFocused() {
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
let item = w.activeFocusItem
if (typeof item.toggle === "function") {
item.toggle()
return
}
if (item.hasOwnProperty("checked")) {
item.checked = !item.checked
return
}
if (typeof item.clicked === "function") {
item.clicked()
return
}
}
function loadFields() { function loadFields() {
selectedCoverPath = "" selectedCoverPath = ""
if (isEditing && game) { if (isEditing && game) {
@ -82,6 +124,45 @@ Kirigami.Dialog {
} }
} }
Connections {
target: GamepadManager
function onNavigateUp() {
if (!dialog.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!dialog.isDescendant(w.activeFocusItem, dialog)) return
dialog.focusNextInChain(false)
}
function onNavigateDown() {
if (!dialog.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!dialog.isDescendant(w.activeFocusItem, dialog)) return
dialog.focusNextInChain(true)
}
function onNavigateLeft() {
if (!dialog.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!dialog.isDescendant(w.activeFocusItem, dialog)) return
dialog.focusNextInChain(false)
}
function onNavigateRight() {
if (!dialog.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!dialog.isDescendant(w.activeFocusItem, dialog)) return
dialog.focusNextInChain(true)
}
function onSelectPressed() {
if (!dialog.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!dialog.isDescendant(w.activeFocusItem, dialog)) return
dialog.activateFocused()
}
}
ColumnLayout { ColumnLayout {
spacing: 0 spacing: 0

View file

@ -4,6 +4,7 @@
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.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.alakarte import org.kde.alakarte
import "components" import "components"
@ -21,20 +22,48 @@ FocusScope {
signal gameSelected(var game) signal gameSelected(var game)
signal gameLaunched(var game) signal gameLaunched(var game)
readonly property int gameCount: proxyModel.count
property url focusedCoverUrl: ""
function focusSearch() { function focusSearch() {
searchField.forceActiveFocus() searchField.forceActiveFocus()
} }
function clearSearch() {
searchField.text = ""
proxyModel.filterText = ""
}
function restoreFocus() { function restoreFocus() {
if (libraryRoot.searchActive) { if (libraryRoot.searchActive) {
libraryRoot.focusSearch() libraryRoot.focusSearch()
} else { } else {
if (libraryRoot.focusedIndex >= 0 && libraryRoot.focusedIndex < proxyModel.count) {
gameGrid.currentIndex = libraryRoot.focusedIndex
}
gameGrid.forceActiveFocus() 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: { onSearchActiveChanged: {
if (!libraryRoot.searchActive) { if (!libraryRoot.searchActive) {
libraryRoot.clearSearch()
Qt.callLater(function() { Qt.callLater(function() {
gameGrid.forceActiveFocus() gameGrid.forceActiveFocus()
}) })
@ -45,6 +74,62 @@ FocusScope {
anchors.fill: parent anchors.fill: parent
anchors.margins: 0 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 { ColumnLayout {
anchors.fill: parent anchors.fill: parent
spacing: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing
@ -93,6 +178,27 @@ FocusScope {
cardSize: libraryRoot.adaptiveCardSize 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 { model: GameSortFilterModel {
id: proxyModel id: proxyModel
sourceModel: App.gameModel sourceModel: App.gameModel
@ -111,9 +217,17 @@ FocusScope {
height: gameGrid.cellHeight height: gameGrid.cellHeight
function clicked() { function clicked() {
gameGrid.currentIndex = index
libraryRoot.focusedIndex = index
card.clicked() card.clicked()
} }
function play() {
gameGrid.currentIndex = index
libraryRoot.focusedIndex = index
card.playClicked()
}
GameCard { GameCard {
id: card id: card
width: gameGrid.cardSize width: gameGrid.cardSize
@ -153,14 +267,13 @@ FocusScope {
anchors.centerIn: parent anchors.centerIn: parent
visible: proxyModel.count === 0 && !App.importing visible: proxyModel.count === 0 && !App.importing
icon: proxyModel.filterText.length > 0 ? "edit-find" : "applications-games" icon: proxyModel.filterText.length > 0 ? "edit-find" : (libraryRoot.filterSource === "favorites" ? "bookmark-new" : (libraryRoot.filterSource === "hidden" ? "view-hidden" : "applications-games"))
title: proxyModel.filterText.length > 0 ? title: proxyModel.filterText.length > 0 ?
i18n("No games found") : i18n("Your library is empty") 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 ? description: proxyModel.filterText.length > 0 ?
i18n("Try adjusting your search") : 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")))
i18n("Import games to get started")
actionText: proxyModel.filterText.length > 0 ? "" : i18n("Import Games") actionText: (proxyModel.filterText.length > 0 || libraryRoot.filterSource === "favorites" || libraryRoot.filterSource === "hidden") ? "" : i18n("Import Games")
onActionTriggered: App.importAllGames() onActionTriggered: App.importAllGames()
} }
@ -170,47 +283,6 @@ FocusScope {
visible: App.importing visible: App.importing
} }
} }
QQC2.ToolBar {
Layout.fillWidth: true
visible: proxyModel.count > 0
leftPadding: 0
rightPadding: 0
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
contentItem: RowLayout {
spacing: Kirigami.Units.mediumSpacing
QQC2.Label {
text: i18np("%1 game", "%1 games", proxyModel.count)
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.smallFont.pointSize
Layout.alignment: Qt.AlignVCenter
}
Item { Layout.fillWidth: true }
QQC2.Slider {
id: sizeSlider
from: 120
to: 280
stepSize: 20
value: App.config.gridSize
Layout.preferredWidth: Kirigami.Units.gridUnit * 8
Layout.alignment: Qt.AlignVCenter
onMoved: App.config.gridSize = value
QQC2.ToolTip {
parent: sizeSlider.handle
visible: sizeSlider.pressed
text: Math.round(sizeSlider.value) + " px"
}
}
}
}
} }
} }
} }

View file

@ -22,6 +22,16 @@ Kirigami.ApplicationWindow {
property string currentSource: "all" property string currentSource: "all"
property bool searchActive: false property bool searchActive: false
readonly property string hintContext: {
if (gameEditDialog.visible) return "edit"
if (detailsSheet.opened) return "details"
if (importSheet.opened) return "import"
if (settingsSheet.opened) return "settings"
if (aboutSheet.opened) return "about"
if (sidebar.modal && sidebar.opened) return "sidebar"
return "library"
}
function closeTopmost() { function closeTopmost() {
if (gameEditDialog.visible) { if (gameEditDialog.visible) {
gameEditDialog.close() gameEditDialog.close()
@ -54,6 +64,8 @@ Kirigami.ApplicationWindow {
} }
if (searchActive) { if (searchActive) {
searchActive = false searchActive = false
libraryView.clearSearch()
Qt.callLater(function() { libraryView.restoreFocus() })
return true return true
} }
return false return false
@ -68,9 +80,30 @@ Kirigami.ApplicationWindow {
&& root.pageStack.layers.depth <= 1 && root.pageStack.layers.depth <= 1
} }
readonly property bool isMobile: Kirigami.Settings.isMobile function uiModeLabel(mode) {
readonly property bool isTablet: Kirigami.Settings.tabletMode && !Kirigami.Settings.isMobile if (mode === Config.Desktop) return i18n("Desktop")
readonly property bool isTouchDevice: Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile if (mode === Config.Handheld) return i18n("Handheld")
return i18n("Automatic")
}
function cycleUiMode() {
if (App.config.uiMode === Config.Auto) {
App.config.uiMode = Config.Desktop
} else if (App.config.uiMode === Config.Desktop) {
App.config.uiMode = Config.Handheld
} else {
App.config.uiMode = Config.Auto
}
root.showPassiveNotification(i18n("UI mode: %1", uiModeLabel(App.config.uiMode)))
}
readonly property bool deviceIsMobile: Kirigami.Settings.isMobile
readonly property bool deviceIsTablet: Kirigami.Settings.tabletMode && !Kirigami.Settings.isMobile
readonly property bool deviceIsTouchDevice: Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile
readonly property bool isMobile: App.config.uiMode === Config.Handheld ? true : deviceIsMobile
readonly property bool isTablet: App.config.uiMode === Config.Handheld ? true : deviceIsTablet
readonly property bool isTouchDevice: App.config.uiMode === Config.Handheld ? true : (App.config.uiMode === Config.Desktop ? false : deviceIsTouchDevice)
readonly property bool isNarrowScreen: width < Kirigami.Units.gridUnit * 30 readonly property bool isNarrowScreen: width < Kirigami.Units.gridUnit * 30
readonly property int adaptiveCardSize: App.config.gridSize readonly property int adaptiveCardSize: App.config.gridSize
@ -78,6 +111,83 @@ Kirigami.ApplicationWindow {
pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar
pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn
footer: QQC2.ToolBar {
visible: true
leftPadding: 0
rightPadding: 0
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
contentItem: Item {
implicitHeight: Math.max(centerHints.implicitHeight, rightExtras.implicitHeight)
readonly property bool compactFooter: (centerHints.implicitWidth + rightExtras.implicitWidth + Kirigami.Units.gridUnit * 2) > width
Kirigami.ShadowedRectangle {
id: centerHints
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
radius: Kirigami.Units.largeSpacing
color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.85)
shadow.size: Kirigami.Units.smallSpacing
shadow.color: Qt.rgba(0, 0, 0, 0.25)
implicitWidth: hintRow.implicitWidth + Kirigami.Units.largeSpacing * 2
implicitHeight: hintRow.implicitHeight + Kirigami.Units.smallSpacing * 2
RowLayout {
id: hintRow
anchors.centerIn: parent
spacing: Kirigami.Units.largeSpacing
BottomHintBar {
context: root.hintContext
Layout.alignment: Qt.AlignVCenter
}
}
}
RowLayout {
id: rightExtras
anchors.right: parent.right
anchors.rightMargin: Kirigami.Units.mediumSpacing
anchors.verticalCenter: parent.verticalCenter
spacing: Kirigami.Units.mediumSpacing
visible: !parent.compactFooter
QQC2.Label {
text: i18np("%1 game", "%1 games", libraryView.gameCount)
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.smallFont.pointSize
Layout.alignment: Qt.AlignVCenter
visible: root.hintContext === "library" && libraryView.gameCount > 0
}
QQC2.Slider {
id: sizeSlider
from: 120
to: 280
stepSize: 20
value: App.config.gridSize
Layout.preferredWidth: Kirigami.Units.gridUnit * 8
Layout.alignment: Qt.AlignVCenter
visible: root.hintContext === "library" && libraryView.gameCount > 0
onMoved: App.config.gridSize = value
QQC2.ToolTip {
parent: sizeSlider.handle
visible: sizeSlider.pressed
text: Math.round(sizeSlider.value) + " px"
}
}
}
}
}
Shortcut { Shortcut {
sequence: "Ctrl+F" sequence: "Ctrl+F"
onActivated: { onActivated: {
@ -134,15 +244,44 @@ Kirigami.ApplicationWindow {
} }
} }
Shortcut {
sequence: "F9"
onActivated: root.cycleUiMode()
}
Connections { Connections {
target: GamepadManager target: GamepadManager
function onBackPressed() { function onBackPressed() {
root.closeTopmost() root.closeTopmost()
} }
function onLeftBumperPressed() {
if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsSheet.opened || aboutSheet.opened) return
if (root.hintContext !== "library" && root.hintContext !== "sidebar") return
sidebarView.cycleSource(-1)
}
function onRightBumperPressed() {
if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsSheet.opened || aboutSheet.opened) return
if (root.hintContext !== "library" && root.hintContext !== "sidebar") return
sidebarView.cycleSource(1)
}
function onDetailsPressed() {
if (gameEditDialog.visible || importSheet.opened || settingsSheet.opened || aboutSheet.opened || (sidebar.modal && sidebar.opened)) return
if (detailsSheet.opened) {
if (root.selectedGame) {
root.selectedGame.favorite = !root.selectedGame.favorite
}
return
}
if (!libraryView || !libraryView.visible) return
libraryView.openDetailsForFocusedGame()
}
function onMenuPressed() { function onMenuPressed() {
if (!settingsSheet.opened) { if (!settingsSheet.opened) {
if (root.canOpenSettings()) { if (root.canOpenSettings()) {
settingsSheet.open() settingsSheet.open()
} else if (detailsSheet.opened) {
detailsSheet.close()
Qt.callLater(function() { settingsSheet.open() })
} else { } else {
root.closeTopmost() root.closeTopmost()
} }
@ -151,6 +290,11 @@ Kirigami.ApplicationWindow {
} }
} }
function onSearchPressed() { function onSearchPressed() {
if (detailsSheet.opened) {
detailsSheet.editRequested()
return
}
if (gameEditDialog.visible || importSheet.opened || settingsSheet.opened || aboutSheet.opened || (sidebar.modal && sidebar.opened)) return
searchActive = true searchActive = true
Qt.callLater(function() { libraryView.focusSearch() }) Qt.callLater(function() { libraryView.focusSearch() })
} }
@ -234,10 +378,12 @@ Kirigami.ApplicationWindow {
onSourceSelected: function(source) { onSourceSelected: function(source) {
root.currentSource = source root.currentSource = source
if (sidebar.modal) { if (sidebar.modal) {
if (!sidebarView.suppressAutoClose) {
sidebar.close() sidebar.close()
Qt.callLater(function() { libraryView.restoreFocus() }) Qt.callLater(function() { libraryView.restoreFocus() })
} }
} }
}
onSettingsRequested: { onSettingsRequested: {
if (sidebar.modal) { if (sidebar.modal) {
@ -278,6 +424,7 @@ Kirigami.ApplicationWindow {
if (root.searchActive) { if (root.searchActive) {
Qt.callLater(function() { libraryView.focusSearch() }) Qt.callLater(function() { libraryView.focusSearch() })
} else { } else {
libraryView.clearSearch()
libraryView.restoreFocus() libraryView.restoreFocus()
} }
} }
@ -293,11 +440,13 @@ Kirigami.ApplicationWindow {
Kirigami.Action { Kirigami.Action {
icon.name: "document-import" icon.name: "document-import"
text: i18n("Import Games") text: i18n("Import Games")
visible: sidebar.modal
onTriggered: importSheet.open() onTriggered: importSheet.open()
}, },
Kirigami.Action { Kirigami.Action {
icon.name: "configure" icon.name: "configure"
text: i18n("Settings") text: i18n("Settings")
visible: sidebar.modal
onTriggered: settingsSheet.open() onTriggered: settingsSheet.open()
} }
] ]
@ -379,6 +528,8 @@ Kirigami.ApplicationWindow {
let p = item let p = item
while (p) { while (p) {
if (p === ancestor) return true if (p === ancestor) return true
if (ancestor.contentItem && p === ancestor.contentItem) return true
if (ancestor.header && p === ancestor.header) return true
p = p.parent p = p.parent
} }
return false return false
@ -421,14 +572,37 @@ Kirigami.ApplicationWindow {
target: GamepadManager target: GamepadManager
function onNavigateUp() { function onNavigateUp() {
if (!importSheet.opened || importScroll.activeFocus) return if (!importSheet.opened || importScroll.activeFocus) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!importSheet.isDescendant(w.activeFocusItem, importSheet)) return
importSheet.focusNextInChain(false) importSheet.focusNextInChain(false)
} }
function onNavigateDown() { function onNavigateDown() {
if (!importSheet.opened || importScroll.activeFocus) return if (!importSheet.opened || importScroll.activeFocus) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!importSheet.isDescendant(w.activeFocusItem, importSheet)) return
importSheet.focusNextInChain(true)
}
function onNavigateLeft() {
if (!importSheet.opened || importScroll.activeFocus) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!importSheet.isDescendant(w.activeFocusItem, importSheet)) return
importSheet.focusNextInChain(false)
}
function onNavigateRight() {
if (!importSheet.opened || importScroll.activeFocus) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!importSheet.isDescendant(w.activeFocusItem, importSheet)) return
importSheet.focusNextInChain(true) importSheet.focusNextInChain(true)
} }
function onSelectPressed() { function onSelectPressed() {
if (!importSheet.opened || importScroll.activeFocus) return if (!importSheet.opened || importScroll.activeFocus) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!importSheet.isDescendant(w.activeFocusItem, importSheet)) return
importSheet.activateFocused() importSheet.activateFocused()
} }
} }
@ -497,7 +671,7 @@ Kirigami.ApplicationWindow {
secondary: "steam" secondary: "steam"
resourceFallback: Qt.resolvedUrl("icons/brand/steam-symbolic.svg") resourceFallback: Qt.resolvedUrl("icons/brand/steam-symbolic.svg")
} }
enabled: !App.importing enabled: !App.importing && App.config.importSteam && !App.gameModel.hasPlatformPrefix("Steam") && App.gameModel.count >= 0
onClicked: App.importFromSteam() onClicked: App.importFromSteam()
} }
@ -511,7 +685,7 @@ Kirigami.ApplicationWindow {
primary: "lutris" primary: "lutris"
secondary: "applications-games" secondary: "applications-games"
} }
enabled: !App.importing enabled: !App.importing && App.config.importLutris && !App.gameModel.hasPlatformPrefix("Lutris") && App.gameModel.count >= 0
onClicked: App.importFromLutris() onClicked: App.importFromLutris()
} }
@ -525,7 +699,7 @@ Kirigami.ApplicationWindow {
primary: "com.heroicgameslauncher.hgl" primary: "com.heroicgameslauncher.hgl"
secondary: "applications-games" secondary: "applications-games"
} }
enabled: !App.importing enabled: !App.importing && App.config.importHeroic && !App.gameModel.hasPlatformPrefix("Heroic") && App.gameModel.count >= 0
onClicked: App.importFromHeroic() onClicked: App.importFromHeroic()
} }
@ -539,7 +713,7 @@ Kirigami.ApplicationWindow {
primary: "user-desktop" primary: "user-desktop"
secondary: "computer" secondary: "computer"
} }
enabled: !App.importing enabled: !App.importing && App.config.importDesktop && !App.gameModel.hasPlatformPrefix("Desktop") && App.gameModel.count >= 0
onClicked: App.importFromDesktop() onClicked: App.importFromDesktop()
} }
@ -553,7 +727,7 @@ Kirigami.ApplicationWindow {
primary: "com.usebottles.bottles" primary: "com.usebottles.bottles"
secondary: "application-x-executable" secondary: "application-x-executable"
} }
enabled: !App.importing enabled: !App.importing && App.config.importBottles && !App.gameModel.hasPlatformPrefix("Bottles") && App.gameModel.count >= 0
onClicked: App.importFromBottles() onClicked: App.importFromBottles()
} }
@ -567,7 +741,7 @@ Kirigami.ApplicationWindow {
primary: "flatpak-discover" primary: "flatpak-discover"
secondary: "applications-games" secondary: "applications-games"
} }
enabled: !App.importing enabled: !App.importing && App.config.importFlatpak && !App.gameModel.hasPlatformPrefix("Flatpak") && App.gameModel.count >= 0
onClicked: App.importFromFlatpak() onClicked: App.importFromFlatpak()
} }
@ -582,7 +756,7 @@ Kirigami.ApplicationWindow {
secondary: "itch" secondary: "itch"
resourceFallback: Qt.resolvedUrl("icons/brand/itchdotio-symbolic.svg") resourceFallback: Qt.resolvedUrl("icons/brand/itchdotio-symbolic.svg")
} }
enabled: !App.importing enabled: !App.importing && App.config.importItch && !App.gameModel.hasPlatformPrefix("itch.io") && App.gameModel.count >= 0
onClicked: App.importFromItch() onClicked: App.importFromItch()
} }
@ -596,7 +770,7 @@ Kirigami.ApplicationWindow {
primary: "legendary" primary: "legendary"
secondary: "applications-games" secondary: "applications-games"
} }
enabled: !App.importing enabled: !App.importing && App.config.importLegendary && !App.gameModel.hasPlatformPrefix("Legendary") && App.gameModel.count >= 0
onClicked: App.importFromLegendary() onClicked: App.importFromLegendary()
} }
@ -611,7 +785,7 @@ Kirigami.ApplicationWindow {
secondary: "retroarch" secondary: "retroarch"
resourceFallback: Qt.resolvedUrl("icons/brand/retroarch-symbolic.svg") resourceFallback: Qt.resolvedUrl("icons/brand/retroarch-symbolic.svg")
} }
enabled: !App.importing enabled: !App.importing && App.config.importRetroArch && !App.gameModel.hasPlatformPrefix("RetroArch") && App.gameModel.count >= 0
onClicked: App.importFromRetroArch() onClicked: App.importFromRetroArch()
} }
} }
@ -623,7 +797,15 @@ Kirigami.ApplicationWindow {
QQC2.Button { QQC2.Button {
text: i18n("Import All") text: i18n("Import All")
icon.name: "document-import" icon.name: "document-import"
enabled: !App.importing enabled: !App.importing && App.gameModel.count >= 0 && ((App.config.importSteam && !App.gameModel.hasPlatformPrefix("Steam"))
|| (App.config.importLutris && !App.gameModel.hasPlatformPrefix("Lutris"))
|| (App.config.importHeroic && !App.gameModel.hasPlatformPrefix("Heroic"))
|| (App.config.importDesktop && !App.gameModel.hasPlatformPrefix("Desktop"))
|| (App.config.importBottles && !App.gameModel.hasPlatformPrefix("Bottles"))
|| (App.config.importFlatpak && !App.gameModel.hasPlatformPrefix("Flatpak"))
|| (App.config.importItch && !App.gameModel.hasPlatformPrefix("itch.io"))
|| (App.config.importLegendary && !App.gameModel.hasPlatformPrefix("Legendary"))
|| (App.config.importRetroArch && !App.gameModel.hasPlatformPrefix("RetroArch")))
onClicked: App.importAllGames() onClicked: App.importAllGames()
} }
@ -649,7 +831,7 @@ Kirigami.ApplicationWindow {
} }
implicitHeight: { implicitHeight: {
if (root.isMobile) return applicationWindow().height if (root.isMobile) return applicationWindow().height
return Math.min(applicationWindow().height - Kirigami.Units.gridUnit * 2, Kirigami.Units.gridUnit * 42) return Math.min(applicationWindow().height - Kirigami.Units.gridUnit * 2, settingsContent.implicitHeight + Kirigami.Units.gridUnit * 4)
} }
onOpened: settingsContent.focusFirstControl() onOpened: settingsContent.focusFirstControl()
@ -697,6 +879,8 @@ Kirigami.ApplicationWindow {
let p = item let p = item
while (p) { while (p) {
if (p === ancestor) return true if (p === ancestor) return true
if (ancestor.contentItem && p === ancestor.contentItem) return true
if (ancestor.header && p === ancestor.header) return true
p = p.parent p = p.parent
} }
return false return false
@ -815,8 +999,91 @@ Kirigami.ApplicationWindow {
return Math.min(applicationWindow().height - Kirigami.Units.gridUnit * 2, Kirigami.Units.gridUnit * 42) return Math.min(applicationWindow().height - Kirigami.Units.gridUnit * 2, Kirigami.Units.gridUnit * 42)
} }
onOpened: aboutCloseButton.forceActiveFocus()
onClosed: libraryView.restoreFocus() onClosed: libraryView.restoreFocus()
function isDescendant(item, ancestor) {
let p = item
while (p) {
if (p === ancestor) return true
if (ancestor.contentItem && p === ancestor.contentItem) return true
if (ancestor.header && p === ancestor.header) return true
p = p.parent
}
return false
}
function focusNextInChain(forward) {
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
let next = w.activeFocusItem
for (let i = 0; i < 50; i++) {
next = next.nextItemInFocusChain(forward)
if (!next) return
if (aboutSheet.isDescendant(next, aboutSheet)) {
next.forceActiveFocus()
return
}
}
}
function activateFocused() {
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
let item = w.activeFocusItem
if (typeof item.toggle === "function") {
item.toggle()
return
}
if (item.hasOwnProperty("checked")) {
item.checked = !item.checked
return
}
if (typeof item.clicked === "function") {
item.clicked()
return
}
}
Connections {
target: GamepadManager
function onNavigateUp() {
if (!aboutSheet.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return
aboutSheet.focusNextInChain(false)
}
function onNavigateDown() {
if (!aboutSheet.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return
aboutSheet.focusNextInChain(true)
}
function onNavigateLeft() {
if (!aboutSheet.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return
aboutSheet.focusNextInChain(false)
}
function onNavigateRight() {
if (!aboutSheet.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return
aboutSheet.focusNextInChain(true)
}
function onSelectPressed() {
if (!aboutSheet.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return
aboutSheet.activateFocused()
}
}
header: Kirigami.ShadowedRectangle { header: Kirigami.ShadowedRectangle {
id: aboutHeader id: aboutHeader
implicitWidth: aboutSheet.implicitWidth implicitWidth: aboutSheet.implicitWidth
@ -844,6 +1111,7 @@ Kirigami.ApplicationWindow {
} }
QQC2.ToolButton { QQC2.ToolButton {
id: aboutCloseButton
text: i18n("Close") text: i18n("Close")
icon.name: "dialog-close" icon.name: "dialog-close"
display: QQC2.AbstractButton.IconOnly display: QQC2.AbstractButton.IconOnly

View file

@ -31,6 +31,45 @@ ColumnLayout {
checked: App.config.showPlatformBadges checked: App.config.showPlatformBadges
onToggled: App.config.showPlatformBadges = checked onToggled: App.config.showPlatformBadges = checked
} }
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
id: uiModeDelegate
text: i18n("UI mode")
description: {
if (App.config.uiMode === Config.Desktop) return i18n("Desktop")
if (App.config.uiMode === Config.Handheld) return i18n("Handheld")
return i18n("Automatic")
}
icon.name: "view-fullscreen"
onClicked: uiModeMenu.open()
QQC2.Menu {
id: uiModeMenu
QQC2.MenuItem {
text: i18n("Automatic")
checkable: true
checked: App.config.uiMode === Config.Auto
onTriggered: App.config.uiMode = Config.Auto
}
QQC2.MenuItem {
text: i18n("Desktop")
checkable: true
checked: App.config.uiMode === Config.Desktop
onTriggered: App.config.uiMode = Config.Desktop
}
QQC2.MenuItem {
text: i18n("Handheld")
checkable: true
checked: App.config.uiMode === Config.Handheld
onTriggered: App.config.uiMode = Config.Handheld
}
}
}
} }
FormCard.FormHeader { FormCard.FormHeader {
@ -293,7 +332,15 @@ ColumnLayout {
text: i18n("Import All Games") text: i18n("Import All Games")
description: i18n("Scan all enabled sources") description: i18n("Scan all enabled sources")
icon.name: "document-import" icon.name: "document-import"
enabled: !App.importing enabled: !App.importing && App.gameModel.count >= 0 && ((App.config.importSteam && !App.gameModel.hasPlatformPrefix("Steam"))
|| (App.config.importLutris && !App.gameModel.hasPlatformPrefix("Lutris"))
|| (App.config.importHeroic && !App.gameModel.hasPlatformPrefix("Heroic"))
|| (App.config.importDesktop && !App.gameModel.hasPlatformPrefix("Desktop"))
|| (App.config.importBottles && !App.gameModel.hasPlatformPrefix("Bottles"))
|| (App.config.importFlatpak && !App.gameModel.hasPlatformPrefix("Flatpak"))
|| (App.config.importItch && !App.gameModel.hasPlatformPrefix("itch.io"))
|| (App.config.importLegendary && !App.gameModel.hasPlatformPrefix("Legendary"))
|| (App.config.importRetroArch && !App.gameModel.hasPlatformPrefix("RetroArch")))
onClicked: App.importAllGames() onClicked: App.importAllGames()
} }

View file

@ -14,6 +14,7 @@ ColumnLayout {
property string currentSource: "all" property string currentSource: "all"
property string currentSourceName: i18n("All Games") property string currentSourceName: i18n("All Games")
property bool suppressAutoClose: false
readonly property int adaptiveFocusRingWidth: 1 readonly property int adaptiveFocusRingWidth: 1
@ -33,6 +34,28 @@ ColumnLayout {
} }
} }
function applySourceAtIndex(i) {
if (i < 0 || i >= sourceModel.count) return
let item = sourceModel.get(i)
if (!item) return
sourceList.currentIndex = i
sidebarRoot.currentSource = item.sourceId
sidebarRoot.currentSourceName = item.name
sidebarRoot.sourceSelected(item.sourceId)
sourceList.positionViewAtIndex(i, ListView.Contain)
}
function cycleSource(delta) {
if (sourceModel.count <= 0) return
let i = sourceList.currentIndex
if (i < 0) i = 0
i = (i + delta) % sourceModel.count
if (i < 0) i += sourceModel.count
sidebarRoot.suppressAutoClose = true
applySourceAtIndex(i)
Qt.callLater(function() { sidebarRoot.suppressAutoClose = false })
}
function iconInfoForPlatform(platformName) { function iconInfoForPlatform(platformName) {
let p = (platformName || "").toLowerCase() let p = (platformName || "").toLowerCase()
@ -93,6 +116,21 @@ ColumnLayout {
border.width: (sourceList.activeFocus && ListView.isCurrentItem) ? sidebarRoot.adaptiveFocusRingWidth : 0 border.width: (sourceList.activeFocus && ListView.isCurrentItem) ? sidebarRoot.adaptiveFocusRingWidth : 0
border.color: Kirigami.Theme.highlightColor border.color: Kirigami.Theme.highlightColor
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: Kirigami.Units.smallSpacing
width: Kirigami.Units.smallSpacing
radius: width / 2
color: Kirigami.Theme.highlightColor
opacity: sourceDelegate.highlighted ? 0.9 : (sourceDelegate.hovered ? 0.45 : 0.0)
Behavior on opacity {
NumberAnimation { duration: Kirigami.Units.shortDuration }
}
}
Behavior on opacity { Behavior on opacity {
NumberAnimation { duration: Kirigami.Units.shortDuration } NumberAnimation { duration: Kirigami.Units.shortDuration }
} }

View file

@ -0,0 +1,280 @@
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.alakarte
RowLayout {
id: root
spacing: Kirigami.Units.largeSpacing
readonly property bool useGamepadHints: GamepadManager.connected
readonly property int style: GamepadManager.controllerStyle
property string context: "library"
function iconBasePath() {
if (!useGamepadHints) return ""
switch (style) {
case GamepadManager.PlayStationController:
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/playstation/"
case GamepadManager.XboxController:
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/xbox/"
case GamepadManager.NintendoController:
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/nintendo/"
default:
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/"
}
}
function iconForButton(buttonLabel) {
if (!useGamepadHints) return ""
const base = iconBasePath()
if (style === GamepadManager.PlayStationController) {
if (buttonLabel === "Cross") return base + "cross.svg"
if (buttonLabel === "Circle") return base + "circle.svg"
if (buttonLabel === "Square") return base + "square.svg"
if (buttonLabel === "Triangle") return base + "triangle.svg"
}
if (style === GamepadManager.XboxController || style === GamepadManager.NintendoController) {
return base + buttonLabel.toLowerCase() + ".svg"
}
if (buttonLabel === "A") return base + "south.svg"
if (buttonLabel === "B") return base + "east.svg"
if (buttonLabel === "X") return base + "west.svg"
if (buttonLabel === "Y") return base + "north.svg"
return base + "south.svg"
}
function iconForAux(action) {
if (!useGamepadHints) return ""
if (action === "lb") {
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/lb.svg"
}
if (action === "rb") {
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/rb.svg"
}
if (action === "menu") {
if (style === GamepadManager.PlayStationController) {
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/playstation/options.svg"
}
if (style === GamepadManager.NintendoController) {
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/nintendo/plus.svg"
}
if (style === GamepadManager.XboxController) {
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/xbox/menu.svg"
}
return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/menu.svg"
}
return ""
}
function keyboardLabel(action) {
if (root.context === "library") {
switch (action) {
case "confirm": return "Space"
case "back": return "Esc"
case "details": return "Enter"
case "search": return "Ctrl+F"
case "menu": return "Ctrl+,"
default: return ""
}
}
if (root.context === "edit") {
switch (action) {
case "confirm": return "Enter"
case "back": return "Esc"
default: return ""
}
}
if (root.context === "details") {
switch (action) {
case "confirm": return "Enter"
case "back": return "Esc"
case "menu": return "Ctrl+,"
default: return ""
}
}
if (root.context === "sidebar") {
switch (action) {
case "confirm": return "Enter"
case "back": return "Esc"
case "menu": return "Ctrl+,"
default: return ""
}
}
if (root.context === "settings" || root.context === "import" || root.context === "sidebar") {
switch (action) {
case "confirm": return "Enter"
case "back": return "Esc"
default: return ""
}
}
return ""
}
function actionLabel(action) {
if (root.context === "library") {
switch (action) {
case "confirm": return i18n("Play")
case "back": return i18n("Back")
case "details": return i18n("Details")
case "search": return i18n("Search")
case "lb": return i18n("Prev Source")
case "rb": return i18n("Next Source")
case "menu": return i18n("Settings")
default: return ""
}
}
if (root.context === "edit") {
switch (action) {
case "confirm": return i18n("Select")
case "back": return i18n("Back")
default: return ""
}
}
if (root.context === "details") {
switch (action) {
case "confirm": return i18n("Play")
case "back": return i18n("Back")
case "details": return i18n("Favorite")
case "search": return i18n("Edit")
case "menu": return i18n("Settings")
default: return ""
}
}
if (root.context === "sidebar") {
switch (action) {
case "confirm": return i18n("Select")
case "back": return i18n("Back")
case "lb": return i18n("Prev Source")
case "rb": return i18n("Next Source")
case "menu": return i18n("Settings")
default: return ""
}
}
if (root.context === "settings" || root.context === "import" || root.context === "sidebar") {
switch (action) {
case "confirm": return i18n("Select")
case "back": return i18n("Back")
default: return ""
}
}
return ""
}
component HintItem: RowLayout {
required property string action
property string label: ""
property string iconSource: ""
property string keyLabel: ""
visible: {
if (root.useGamepadHints) return iconSource != "" && label.length > 0
return keyLabel.length > 0 && label.length > 0
}
spacing: Kirigami.Units.smallSpacing
Rectangle {
id: buttonFrame
radius: Kirigami.Units.smallSpacing
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08)
border.width: 1
border.color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
Layout.preferredHeight: Kirigami.Units.gridUnit * 1.4
Layout.preferredWidth: useGamepadHints
? Layout.preferredHeight
: Math.max(keyText.implicitWidth + Kirigami.Units.mediumSpacing * 2, Layout.preferredHeight)
Image {
id: buttonIcon
anchors.centerIn: parent
width: parent.height * 0.7
height: width
source: parent.parent.iconSource
visible: useGamepadHints && source != ""
sourceSize: Qt.size(width * 2, height * 2)
}
QQC2.Label {
id: keyText
anchors.centerIn: parent
text: parent.parent.keyLabel
font.bold: true
font.pointSize: Kirigami.Theme.smallFont.pointSize
color: Kirigami.Theme.textColor
visible: !useGamepadHints
}
}
QQC2.Label {
text: parent.label
color: Kirigami.Theme.textColor
font.pointSize: Kirigami.Theme.smallFont.pointSize
Layout.alignment: Qt.AlignVCenter
}
}
HintItem {
action: "confirm"
label: actionLabel("confirm")
iconSource: iconForButton(GamepadManager.confirmButtonLabel)
keyLabel: keyboardLabel("confirm")
}
HintItem {
action: "back"
label: actionLabel("back")
iconSource: iconForButton(GamepadManager.backButtonLabel)
keyLabel: keyboardLabel("back")
}
HintItem {
action: "details"
label: actionLabel("details")
iconSource: iconForButton(GamepadManager.detailsButtonLabel)
keyLabel: keyboardLabel("details")
}
HintItem {
action: "search"
label: actionLabel("search")
iconSource: iconForButton(GamepadManager.searchButtonLabel)
keyLabel: keyboardLabel("search")
}
HintItem {
action: "lb"
label: actionLabel("lb")
iconSource: (root.context === "library" || root.context === "sidebar") ? iconForAux("lb") : ""
keyLabel: ""
}
HintItem {
action: "rb"
label: actionLabel("rb")
iconSource: (root.context === "library" || root.context === "sidebar") ? iconForAux("rb") : ""
keyLabel: ""
}
HintItem {
action: "menu"
label: actionLabel("menu")
iconSource: (root.context === "library" || root.context === "details" || root.context === "sidebar") ? iconForAux("menu") : ""
keyLabel: keyboardLabel("menu")
}
}

View file

@ -62,10 +62,14 @@ GridView {
function onNavigateRight() { if (gridView.activeFocus) gridView.navigateRight() } function onNavigateRight() { if (gridView.activeFocus) gridView.navigateRight() }
function onSelectPressed() { function onSelectPressed() {
if (gridView.activeFocus && gridView.currentItem) { if (gridView.activeFocus && gridView.currentItem) {
if (gridView.currentItem.play) {
gridView.currentItem.play()
} else {
gridView.currentItem.clicked() gridView.currentItem.clicked()
} }
} }
} }
}
readonly property int columns: Math.max(1, Math.floor(width / cellWidth)) readonly property int columns: Math.max(1, Math.floor(width / cellWidth))

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<rect x="6" y="11" width="5" height="4" fill="currentColor" opacity="0.6"/>
<rect x="13" y="11" width="5" height="4" fill="currentColor" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 350 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<rect x="9" y="15" width="6" height="7" fill="currentColor" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 287 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<rect x="2" y="9" width="7" height="6" fill="currentColor" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 286 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<rect x="15" y="9" width="7" height="6" fill="currentColor" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 287 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<rect x="9" y="2" width="6" height="7" fill="currentColor" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 286 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 209 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M7 12h7M11 9l3 3-3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="8" fill="currentColor">LB</text>
</svg>

After

Width:  |  Height:  |  Size: 324 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
<circle cx="12" cy="12" r="5" fill="currentColor" opacity="0.3"/>
<circle cx="12" cy="12" r="3" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 298 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M4 18V8c0-2.2 1.8-4 4-4h8c2.2 0 4 1.8 4 4v10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<text x="12" y="16" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="8" fill="currentColor">LT</text>
</svg>

After

Width:  |  Height:  |  Size: 354 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<line x1="7" y1="11" x2="17" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="7" y1="15" x2="17" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 403 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M12 17v-7M15 13l-3-3-3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 313 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="8" fill="currentColor">RB</text>
</svg>

After

Width:  |  Height:  |  Size: 324 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
<circle cx="12" cy="12" r="5" fill="currentColor" opacity="0.3"/>
<circle cx="12" cy="12" r="3" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 298 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M4 18V8c0-2.2 1.8-4 4-4h8c2.2 0 4 1.8 4 4v10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<text x="12" y="16" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="8" fill="currentColor">RT</text>
</svg>

After

Width:  |  Height:  |  Size: 354 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M12 7v7M9 11l3 3 3-3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M17 12h-7M13 15l-3-3 3-3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 313 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">A</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">B</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M12 7l5 4v6h-3v-4h-4v4H7v-6z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 296 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
<line x1="7" y1="12" x2="17" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 279 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
<line x1="12" y1="7" x2="12" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="7" y1="12" x2="17" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 382 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">X</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">Y</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<circle cx="12" cy="12" r="5" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 261 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 8l8 8M16 8l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 271 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<line x1="7" y1="11" x2="17" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="7" y1="15" x2="17" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 403 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 13l4-3 4 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<line x1="12" y1="10" x2="12" y2="16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 427 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<rect x="7" y="7" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" rx="1"/>
</svg>

After

Width:  |  Height:  |  Size: 279 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M12 6l6.5 11h-13z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 283 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">A</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">B</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<line x1="7" y1="11" x2="17" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="7" y1="15" x2="17" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 403 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<rect x="6" y="11" width="5" height="4" fill="currentColor" opacity="0.6"/>
<rect x="13" y="11" width="5" height="4" fill="currentColor" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 350 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">X</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">Y</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B