mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-29 07:03:08 +00:00
Add cover art, search, filters, and recently played
Grid tiles now show Steam library artwork when available, falling back to icon+label for games without cover art. Cell proportions adjusted to 2:3 for portrait covers. Search bar filters the library by name. Source tabs filter by All/Steam/Desktop. Both properties live in C++ so QML just binds filterString and sourceFilter. "Continue Playing" row shows the last 5 launched games with artwork, persisted across sessions via plasmamobilerc. Remove orphaned GameTile.qml (replaced by inline delegate).
This commit is contained in:
parent
1bf8cfb2ba
commit
976c770af4
6 changed files with 393 additions and 138 deletions
|
|
@ -18,6 +18,7 @@ target_link_libraries(gamingshellplugin PRIVATE
|
|||
KF6::I18n
|
||||
KF6::Service
|
||||
KF6::CoreAddons
|
||||
KF6::ConfigCore
|
||||
SDL3::SDL3
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
|
||||
#include "gamelauncherprovider.h"
|
||||
|
||||
#include <KConfigGroup>
|
||||
#include <KService>
|
||||
#include <KSharedConfig>
|
||||
#include <KShell>
|
||||
#include <KSycoca>
|
||||
|
||||
|
|
@ -15,8 +17,11 @@
|
|||
#include <QStandardPaths>
|
||||
#include <QTextStream>
|
||||
|
||||
static const QString s_recentGroup = QStringLiteral("GamingRecentlyPlayed");
|
||||
|
||||
GameLauncherProvider::GameLauncherProvider(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_config(KSharedConfig::openConfig(QStringLiteral("plasmamobilerc")))
|
||||
{
|
||||
connect(KSycoca::self(), &KSycoca::databaseChanged, this, &GameLauncherProvider::refresh);
|
||||
refresh();
|
||||
|
|
@ -80,23 +85,22 @@ void GameLauncherProvider::refresh()
|
|||
m_loading = true;
|
||||
Q_EMIT loadingChanged();
|
||||
|
||||
beginResetModel();
|
||||
m_games.clear();
|
||||
m_allGames.clear();
|
||||
|
||||
loadDesktopGames();
|
||||
loadSteamGames();
|
||||
loadFlatpakGames();
|
||||
loadRecentTimestamps();
|
||||
|
||||
// Sort alphabetically, case-insensitive
|
||||
std::sort(m_games.begin(), m_games.end(), [](const GameEntry &a, const GameEntry &b) {
|
||||
std::sort(m_allGames.begin(), m_allGames.end(), [](const GameEntry &a, const GameEntry &b) {
|
||||
return a.name.compare(b.name, Qt::CaseInsensitive) < 0;
|
||||
});
|
||||
|
||||
endResetModel();
|
||||
applyFilter();
|
||||
|
||||
m_loading = false;
|
||||
Q_EMIT loadingChanged();
|
||||
Q_EMIT countChanged();
|
||||
}
|
||||
|
||||
void GameLauncherProvider::launch(int index)
|
||||
|
|
@ -128,6 +132,54 @@ void GameLauncherProvider::launch(int index)
|
|||
}
|
||||
|
||||
Q_EMIT gameLaunched(g.name);
|
||||
|
||||
// Record timestamp for "recently played"
|
||||
saveRecentTimestamp(g.storageId, QDateTime::currentDateTime());
|
||||
|
||||
// Update the in-memory entry so recentGames() picks it up immediately
|
||||
for (auto &entry : m_allGames) {
|
||||
if (entry.storageId == g.storageId) {
|
||||
entry.lastPlayed = QDateTime::currentDateTime();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameLauncherProvider::launchByStorageId(const QString &storageId)
|
||||
{
|
||||
for (int i = 0; i < m_allGames.size(); ++i) {
|
||||
if (m_allGames.at(i).storageId == storageId) {
|
||||
// Find the index in the filtered model, or launch from allGames directly
|
||||
for (int j = 0; j < m_games.size(); ++j) {
|
||||
if (m_games.at(j).storageId == storageId) {
|
||||
launch(j);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Not in filtered view — launch directly from allGames
|
||||
const auto &g = m_allGames.at(i);
|
||||
if (g.source == QLatin1String("desktop")) {
|
||||
auto service = KService::serviceByStorageId(g.storageId);
|
||||
if (service) {
|
||||
QStringList args = KShell::splitArgs(service->exec());
|
||||
if (!args.isEmpty()) {
|
||||
QString program = args.takeFirst();
|
||||
QProcess::startDetached(program, args);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
QStringList parts = g.launchCommand.split(QLatin1Char(' '));
|
||||
if (!parts.isEmpty()) {
|
||||
QString program = parts.takeFirst();
|
||||
QProcess::startDetached(program, parts);
|
||||
}
|
||||
}
|
||||
Q_EMIT gameLaunched(g.name);
|
||||
saveRecentTimestamp(g.storageId, QDateTime::currentDateTime());
|
||||
m_allGames[i].lastPlayed = QDateTime::currentDateTime();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- XDG .desktop games ---
|
||||
|
|
@ -158,7 +210,7 @@ void GameLauncherProvider::loadDesktopGames()
|
|||
entry.storageId = service->storageId();
|
||||
entry.launchCommand = service->exec();
|
||||
entry.installed = true;
|
||||
m_games.append(entry);
|
||||
m_allGames.append(entry);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -267,7 +319,7 @@ void GameLauncherProvider::loadSteamGames()
|
|||
}
|
||||
}
|
||||
|
||||
m_games.append(entry);
|
||||
m_allGames.append(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -282,3 +334,103 @@ void GameLauncherProvider::loadFlatpakGames()
|
|||
// (e.g. querying flatpak metadata for games that don't set
|
||||
// the Game category properly).
|
||||
}
|
||||
|
||||
QString GameLauncherProvider::filterString() const
|
||||
{
|
||||
return m_filterString;
|
||||
}
|
||||
|
||||
void GameLauncherProvider::setFilterString(const QString &filter)
|
||||
{
|
||||
if (m_filterString == filter) {
|
||||
return;
|
||||
}
|
||||
m_filterString = filter;
|
||||
Q_EMIT filterStringChanged();
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
QString GameLauncherProvider::sourceFilter() const
|
||||
{
|
||||
return m_sourceFilter;
|
||||
}
|
||||
|
||||
void GameLauncherProvider::setSourceFilter(const QString &source)
|
||||
{
|
||||
if (m_sourceFilter == source) {
|
||||
return;
|
||||
}
|
||||
m_sourceFilter = source;
|
||||
Q_EMIT sourceFilterChanged();
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
void GameLauncherProvider::applyFilter()
|
||||
{
|
||||
beginResetModel();
|
||||
m_games.clear();
|
||||
|
||||
for (const auto &g : std::as_const(m_allGames)) {
|
||||
if (!m_sourceFilter.isEmpty() && g.source != m_sourceFilter) {
|
||||
continue;
|
||||
}
|
||||
if (!m_filterString.isEmpty() && !g.name.contains(m_filterString, Qt::CaseInsensitive)) {
|
||||
continue;
|
||||
}
|
||||
m_games.append(g);
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
Q_EMIT countChanged();
|
||||
}
|
||||
|
||||
void GameLauncherProvider::loadRecentTimestamps()
|
||||
{
|
||||
const KConfigGroup group(m_config, s_recentGroup);
|
||||
for (auto &entry : m_allGames) {
|
||||
const QString key = entry.storageId;
|
||||
if (group.hasKey(key)) {
|
||||
entry.lastPlayed = group.readEntry(key, QDateTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameLauncherProvider::saveRecentTimestamp(const QString &storageId, const QDateTime &when)
|
||||
{
|
||||
KConfigGroup group(m_config, s_recentGroup);
|
||||
group.writeEntry(storageId, when);
|
||||
group.sync();
|
||||
}
|
||||
|
||||
QVariantList GameLauncherProvider::recentGames(int limit) const
|
||||
{
|
||||
// Gather entries that have been launched at least once
|
||||
QList<const GameEntry *> recent;
|
||||
for (const auto &g : m_allGames) {
|
||||
if (g.lastPlayed.isValid()) {
|
||||
recent.append(&g);
|
||||
}
|
||||
}
|
||||
|
||||
// Most recent first
|
||||
std::sort(recent.begin(), recent.end(), [](const GameEntry *a, const GameEntry *b) {
|
||||
return a->lastPlayed > b->lastPlayed;
|
||||
});
|
||||
|
||||
if (recent.size() > limit) {
|
||||
recent.resize(limit);
|
||||
}
|
||||
|
||||
QVariantList result;
|
||||
result.reserve(recent.size());
|
||||
for (const auto *g : recent) {
|
||||
QVariantMap map;
|
||||
map[QStringLiteral("name")] = g->name;
|
||||
map[QStringLiteral("icon")] = g->icon;
|
||||
map[QStringLiteral("source")] = g->source;
|
||||
map[QStringLiteral("storageId")] = g->storageId;
|
||||
map[QStringLiteral("artwork")] = g->artwork;
|
||||
result.append(map);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@
|
|||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QDateTime>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <qqmlregistration.h>
|
||||
|
||||
#include <KSharedConfig>
|
||||
|
||||
class GameLauncherProvider : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -16,6 +19,8 @@ class GameLauncherProvider : public QAbstractListModel
|
|||
|
||||
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
|
||||
Q_PROPERTY(QString filterString READ filterString WRITE setFilterString NOTIFY filterStringChanged)
|
||||
Q_PROPERTY(QString sourceFilter READ sourceFilter WRITE setSourceFilter NOTIFY sourceFilterChanged)
|
||||
|
||||
public:
|
||||
explicit GameLauncherProvider(QObject *parent = nullptr);
|
||||
|
|
@ -37,13 +42,21 @@ public:
|
|||
|
||||
int count() const;
|
||||
bool loading() const;
|
||||
QString filterString() const;
|
||||
void setFilterString(const QString &filter);
|
||||
QString sourceFilter() const;
|
||||
void setSourceFilter(const QString &source);
|
||||
|
||||
Q_INVOKABLE void refresh();
|
||||
Q_INVOKABLE void launch(int index);
|
||||
Q_INVOKABLE void launchByStorageId(const QString &storageId);
|
||||
Q_INVOKABLE QVariantList recentGames(int limit = 5) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void countChanged();
|
||||
void loadingChanged();
|
||||
void filterStringChanged();
|
||||
void sourceFilterChanged();
|
||||
void gameLaunched(const QString &name);
|
||||
|
||||
private:
|
||||
|
|
@ -54,13 +67,21 @@ private:
|
|||
QString storageId;
|
||||
QString launchCommand;
|
||||
QString artwork;
|
||||
QDateTime lastPlayed;
|
||||
bool installed = true;
|
||||
};
|
||||
|
||||
void loadDesktopGames();
|
||||
void loadSteamGames();
|
||||
void loadFlatpakGames();
|
||||
void loadRecentTimestamps();
|
||||
void saveRecentTimestamp(const QString &storageId, const QDateTime &when);
|
||||
void applyFilter();
|
||||
|
||||
QList<GameEntry> m_games;
|
||||
QList<GameEntry> m_allGames;
|
||||
QList<GameEntry> m_games; // filtered view
|
||||
QString m_filterString;
|
||||
QString m_sourceFilter; // empty = all, or "desktop"/"steam"/"flatpak"
|
||||
KSharedConfigPtr m_config;
|
||||
bool m_loading = false;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -78,7 +78,6 @@ ecm_target_qml_sources(org.kde.plasma.mobile.homescreen.folio SOURCES
|
|||
|
||||
ecm_target_qml_sources(org.kde.plasma.mobile.homescreen.folio SOURCES
|
||||
qml/gaming/GameCenterOverlay.qml
|
||||
qml/gaming/GameTile.qml
|
||||
qml/gaming/GamingHUD.qml
|
||||
qml/gaming/RunningGamesView.qml
|
||||
PATH gaming
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ Window {
|
|||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
GamingShell.GameLauncherProvider.filterString = ""
|
||||
GamingShell.GameLauncherProvider.sourceFilter = ""
|
||||
GamingShell.GameLauncherProvider.refresh()
|
||||
if (runningGames.hasTasks) {
|
||||
runningGames.focusFirstTask()
|
||||
|
|
@ -155,12 +157,123 @@ Window {
|
|||
onMoveDownRequested: grid.forceActiveFocus()
|
||||
}
|
||||
|
||||
// ---- game grid ----
|
||||
Kirigami.Heading {
|
||||
level: 2
|
||||
text: i18n("Library")
|
||||
// ---- continue playing ----
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
visible: recentList.count > 0 && !runningGames.hasTasks
|
||||
|
||||
Kirigami.Heading {
|
||||
level: 2
|
||||
text: i18n("Continue Playing")
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: recentList
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 5
|
||||
orientation: ListView.Horizontal
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
clip: true
|
||||
|
||||
model: root.visible ? GamingShell.GameLauncherProvider.recentGames(5) : []
|
||||
|
||||
delegate: QQC2.ItemDelegate {
|
||||
width: Kirigami.Units.gridUnit * 7
|
||||
height: recentList.height
|
||||
|
||||
required property var modelData
|
||||
|
||||
readonly property bool hasArt: modelData.artwork && modelData.artwork.length > 0
|
||||
|
||||
background: Rectangle {
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: parent.hovered ? Kirigami.Theme.hoverColor : "transparent"
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Image {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
source: hasArt ? "file://" + modelData.artwork : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: hasArt
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
Kirigami.Icon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitWidth: Kirigami.Units.iconSizes.large
|
||||
implicitHeight: Kirigami.Units.iconSizes.large
|
||||
source: modelData.icon
|
||||
visible: !hasArt
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.name
|
||||
maximumLineCount: 1
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
GamingShell.GameLauncherProvider.launchByStorageId(modelData.storageId)
|
||||
root.gameStarted()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- search + filter ----
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.SearchField {
|
||||
id: searchField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: i18n("Search games…")
|
||||
onTextChanged: GamingShell.GameLauncherProvider.filterString = text
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
if (text.length > 0) {
|
||||
clear()
|
||||
} else {
|
||||
root.dismissRequested()
|
||||
}
|
||||
}
|
||||
Keys.onDownPressed: grid.forceActiveFocus()
|
||||
}
|
||||
|
||||
QQC2.TabBar {
|
||||
id: sourceFilterBar
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
QQC2.TabButton {
|
||||
text: i18n("All")
|
||||
width: implicitWidth
|
||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = ""
|
||||
}
|
||||
QQC2.TabButton {
|
||||
text: "Steam"
|
||||
width: implicitWidth
|
||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "steam"
|
||||
}
|
||||
QQC2.TabButton {
|
||||
text: i18n("Desktop")
|
||||
width: implicitWidth
|
||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "desktop"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- game grid ----
|
||||
|
||||
GridView {
|
||||
id: grid
|
||||
|
||||
|
|
@ -169,11 +282,11 @@ Window {
|
|||
|
||||
model: GamingShell.GameLauncherProvider
|
||||
|
||||
readonly property real minCellSize: Kirigami.Units.gridUnit * 7
|
||||
readonly property real minCellSize: Kirigami.Units.gridUnit * 8
|
||||
readonly property int columns: Math.max(2, Math.floor(width / minCellSize))
|
||||
|
||||
cellWidth: Math.floor(width / columns)
|
||||
cellHeight: cellWidth + Kirigami.Units.gridUnit * 2
|
||||
cellHeight: Math.floor(cellWidth * 1.5) + Kirigami.Units.gridUnit * 2
|
||||
|
||||
keyNavigationEnabled: true
|
||||
highlightMoveDuration: 0
|
||||
|
|
@ -208,9 +321,14 @@ Window {
|
|||
required property string name
|
||||
required property string icon
|
||||
required property string source
|
||||
required property string artwork
|
||||
|
||||
readonly property bool hasArt: artwork.length > 0
|
||||
|
||||
QQC2.ItemDelegate {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.smallSpacing
|
||||
padding: 0
|
||||
|
||||
readonly property bool isCurrent: GridView.isCurrentItem && grid.activeFocus
|
||||
|
||||
|
|
@ -223,43 +341,99 @@ Window {
|
|||
Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } }
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
contentItem: Item {
|
||||
// ---- cover art tile ----
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
visible: hasArt
|
||||
|
||||
Kirigami.Icon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitWidth: Kirigami.Units.iconSizes.huge
|
||||
implicitHeight: Kirigami.Units.iconSizes.huge
|
||||
source: icon
|
||||
Image {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
source: hasArt ? "file://" + artwork : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
|
||||
scale: parent.parent.isCurrent ? 1.08 : 1.0
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad }
|
||||
// Rounded top corners via layer
|
||||
layer.enabled: true
|
||||
layer.effect: Item {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
scale: parent.parent.parent.isCurrent ? 1.03 : 1.0
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
}
|
||||
|
||||
// Title beneath artwork
|
||||
PC3.Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
text: name
|
||||
maximumLineCount: 1
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: Kirigami.Units.smallSpacing
|
||||
rightPadding: Kirigami.Units.smallSpacing
|
||||
color: parent.parent.parent.isCurrent
|
||||
? Kirigami.Theme.highlightedTextColor
|
||||
: Kirigami.Theme.textColor
|
||||
}
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
text: name
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.Wrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
color: parent.parent.isCurrent
|
||||
? Kirigami.Theme.highlightedTextColor
|
||||
: Kirigami.Theme.textColor
|
||||
}
|
||||
// ---- fallback icon tile ----
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.smallSpacing
|
||||
visible: !hasArt
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
// Source badge
|
||||
PC3.Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: source === "steam" ? "Steam"
|
||||
: source === "flatpak" ? "Flatpak"
|
||||
: ""
|
||||
visible: source !== "desktop"
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
||||
opacity: 0.6
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
Kirigami.Icon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitWidth: Kirigami.Units.iconSizes.huge
|
||||
implicitHeight: Kirigami.Units.iconSizes.huge
|
||||
source: icon
|
||||
|
||||
scale: parent.parent.parent.isCurrent ? 1.08 : 1.0
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
text: name
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.Wrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
color: parent.parent.parent.isCurrent
|
||||
? Kirigami.Theme.highlightedTextColor
|
||||
: Kirigami.Theme.textColor
|
||||
}
|
||||
|
||||
// Source badge
|
||||
PC3.Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: source === "steam" ? "Steam"
|
||||
: source === "flatpak" ? "Flatpak"
|
||||
: ""
|
||||
visible: source !== "desktop"
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
||||
opacity: 0.6
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
||||
import org.kde.plasma.plasmoid
|
||||
|
||||
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
|
||||
import org.kde.plasma.components 3.0 as PC3
|
||||
|
||||
QQC2.ItemDelegate {
|
||||
id: root
|
||||
|
||||
required property var folio
|
||||
required property Folio.FolioApplication application
|
||||
required property bool isCurrent
|
||||
|
||||
signal launchRequested()
|
||||
|
||||
Keys.onReturnPressed: clicked()
|
||||
Keys.onEnterPressed: clicked()
|
||||
|
||||
onClicked: {
|
||||
if (!application) return
|
||||
if (application.icon && typeof application.icon === "string" && application.icon.length > 0 && !application.running) {
|
||||
MobileShellState.ShellDBusClient.openAppLaunchAnimationWithPosition(
|
||||
Plasmoid.screen,
|
||||
application.icon,
|
||||
application.name,
|
||||
application.storageId,
|
||||
iconItem.Kirigami.ScenePosition.x + iconItem.width / 2,
|
||||
iconItem.Kirigami.ScenePosition.y + iconItem.height / 2,
|
||||
Math.min(iconItem.width, iconItem.height))
|
||||
}
|
||||
MobileShell.AppLaunch.launchOrActivateApp(application.storageId)
|
||||
launchRequested()
|
||||
}
|
||||
|
||||
function launch() {
|
||||
clicked()
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Button
|
||||
color: root.isCurrent
|
||||
? Kirigami.Theme.highlightColor
|
||||
: (root.hovered ? Kirigami.Theme.hoverColor : "transparent")
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Icon {
|
||||
id: iconItem
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitWidth: Kirigami.Units.iconSizes.huge
|
||||
implicitHeight: Kirigami.Units.iconSizes.huge
|
||||
source: root.application ? root.application.icon : ""
|
||||
|
||||
scale: root.isCurrent ? 1.08 : 1.0
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
id: nameLabel
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
text: root.application ? root.application.name : ""
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.Wrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
color: root.isCurrent ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Kirigami.Units.shortDuration }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue