mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-07-30 00:04:45 +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::I18n
|
||||||
KF6::Service
|
KF6::Service
|
||||||
KF6::CoreAddons
|
KF6::CoreAddons
|
||||||
|
KF6::ConfigCore
|
||||||
SDL3::SDL3
|
SDL3::SDL3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
|
|
||||||
#include "gamelauncherprovider.h"
|
#include "gamelauncherprovider.h"
|
||||||
|
|
||||||
|
#include <KConfigGroup>
|
||||||
#include <KService>
|
#include <KService>
|
||||||
|
#include <KSharedConfig>
|
||||||
#include <KShell>
|
#include <KShell>
|
||||||
#include <KSycoca>
|
#include <KSycoca>
|
||||||
|
|
||||||
|
|
@ -15,8 +17,11 @@
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
|
|
||||||
|
static const QString s_recentGroup = QStringLiteral("GamingRecentlyPlayed");
|
||||||
|
|
||||||
GameLauncherProvider::GameLauncherProvider(QObject *parent)
|
GameLauncherProvider::GameLauncherProvider(QObject *parent)
|
||||||
: QAbstractListModel(parent)
|
: QAbstractListModel(parent)
|
||||||
|
, m_config(KSharedConfig::openConfig(QStringLiteral("plasmamobilerc")))
|
||||||
{
|
{
|
||||||
connect(KSycoca::self(), &KSycoca::databaseChanged, this, &GameLauncherProvider::refresh);
|
connect(KSycoca::self(), &KSycoca::databaseChanged, this, &GameLauncherProvider::refresh);
|
||||||
refresh();
|
refresh();
|
||||||
|
|
@ -80,23 +85,22 @@ void GameLauncherProvider::refresh()
|
||||||
m_loading = true;
|
m_loading = true;
|
||||||
Q_EMIT loadingChanged();
|
Q_EMIT loadingChanged();
|
||||||
|
|
||||||
beginResetModel();
|
m_allGames.clear();
|
||||||
m_games.clear();
|
|
||||||
|
|
||||||
loadDesktopGames();
|
loadDesktopGames();
|
||||||
loadSteamGames();
|
loadSteamGames();
|
||||||
loadFlatpakGames();
|
loadFlatpakGames();
|
||||||
|
loadRecentTimestamps();
|
||||||
|
|
||||||
// Sort alphabetically, case-insensitive
|
// 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;
|
return a.name.compare(b.name, Qt::CaseInsensitive) < 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
endResetModel();
|
applyFilter();
|
||||||
|
|
||||||
m_loading = false;
|
m_loading = false;
|
||||||
Q_EMIT loadingChanged();
|
Q_EMIT loadingChanged();
|
||||||
Q_EMIT countChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameLauncherProvider::launch(int index)
|
void GameLauncherProvider::launch(int index)
|
||||||
|
|
@ -128,6 +132,54 @@ void GameLauncherProvider::launch(int index)
|
||||||
}
|
}
|
||||||
|
|
||||||
Q_EMIT gameLaunched(g.name);
|
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 ---
|
// --- XDG .desktop games ---
|
||||||
|
|
@ -158,7 +210,7 @@ void GameLauncherProvider::loadDesktopGames()
|
||||||
entry.storageId = service->storageId();
|
entry.storageId = service->storageId();
|
||||||
entry.launchCommand = service->exec();
|
entry.launchCommand = service->exec();
|
||||||
entry.installed = true;
|
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
|
// (e.g. querying flatpak metadata for games that don't set
|
||||||
// the Game category properly).
|
// 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
|
#pragma once
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
#include <QDateTime>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <qqmlregistration.h>
|
#include <qqmlregistration.h>
|
||||||
|
|
||||||
|
#include <KSharedConfig>
|
||||||
|
|
||||||
class GameLauncherProvider : public QAbstractListModel
|
class GameLauncherProvider : public QAbstractListModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
@ -16,6 +19,8 @@ class GameLauncherProvider : public QAbstractListModel
|
||||||
|
|
||||||
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||||
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
|
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:
|
public:
|
||||||
explicit GameLauncherProvider(QObject *parent = nullptr);
|
explicit GameLauncherProvider(QObject *parent = nullptr);
|
||||||
|
|
@ -37,13 +42,21 @@ public:
|
||||||
|
|
||||||
int count() const;
|
int count() const;
|
||||||
bool loading() 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 refresh();
|
||||||
Q_INVOKABLE void launch(int index);
|
Q_INVOKABLE void launch(int index);
|
||||||
|
Q_INVOKABLE void launchByStorageId(const QString &storageId);
|
||||||
|
Q_INVOKABLE QVariantList recentGames(int limit = 5) const;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void countChanged();
|
void countChanged();
|
||||||
void loadingChanged();
|
void loadingChanged();
|
||||||
|
void filterStringChanged();
|
||||||
|
void sourceFilterChanged();
|
||||||
void gameLaunched(const QString &name);
|
void gameLaunched(const QString &name);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
@ -54,13 +67,21 @@ private:
|
||||||
QString storageId;
|
QString storageId;
|
||||||
QString launchCommand;
|
QString launchCommand;
|
||||||
QString artwork;
|
QString artwork;
|
||||||
|
QDateTime lastPlayed;
|
||||||
bool installed = true;
|
bool installed = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
void loadDesktopGames();
|
void loadDesktopGames();
|
||||||
void loadSteamGames();
|
void loadSteamGames();
|
||||||
void loadFlatpakGames();
|
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;
|
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
|
ecm_target_qml_sources(org.kde.plasma.mobile.homescreen.folio SOURCES
|
||||||
qml/gaming/GameCenterOverlay.qml
|
qml/gaming/GameCenterOverlay.qml
|
||||||
qml/gaming/GameTile.qml
|
|
||||||
qml/gaming/GamingHUD.qml
|
qml/gaming/GamingHUD.qml
|
||||||
qml/gaming/RunningGamesView.qml
|
qml/gaming/RunningGamesView.qml
|
||||||
PATH gaming
|
PATH gaming
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,8 @@ Window {
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
|
GamingShell.GameLauncherProvider.filterString = ""
|
||||||
|
GamingShell.GameLauncherProvider.sourceFilter = ""
|
||||||
GamingShell.GameLauncherProvider.refresh()
|
GamingShell.GameLauncherProvider.refresh()
|
||||||
if (runningGames.hasTasks) {
|
if (runningGames.hasTasks) {
|
||||||
runningGames.focusFirstTask()
|
runningGames.focusFirstTask()
|
||||||
|
|
@ -155,12 +157,123 @@ Window {
|
||||||
onMoveDownRequested: grid.forceActiveFocus()
|
onMoveDownRequested: grid.forceActiveFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- game grid ----
|
// ---- continue playing ----
|
||||||
Kirigami.Heading {
|
ColumnLayout {
|
||||||
level: 2
|
Layout.fillWidth: true
|
||||||
text: i18n("Library")
|
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 {
|
GridView {
|
||||||
id: grid
|
id: grid
|
||||||
|
|
||||||
|
|
@ -169,11 +282,11 @@ Window {
|
||||||
|
|
||||||
model: GamingShell.GameLauncherProvider
|
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))
|
readonly property int columns: Math.max(2, Math.floor(width / minCellSize))
|
||||||
|
|
||||||
cellWidth: Math.floor(width / columns)
|
cellWidth: Math.floor(width / columns)
|
||||||
cellHeight: cellWidth + Kirigami.Units.gridUnit * 2
|
cellHeight: Math.floor(cellWidth * 1.5) + Kirigami.Units.gridUnit * 2
|
||||||
|
|
||||||
keyNavigationEnabled: true
|
keyNavigationEnabled: true
|
||||||
highlightMoveDuration: 0
|
highlightMoveDuration: 0
|
||||||
|
|
@ -208,9 +321,14 @@ Window {
|
||||||
required property string name
|
required property string name
|
||||||
required property string icon
|
required property string icon
|
||||||
required property string source
|
required property string source
|
||||||
|
required property string artwork
|
||||||
|
|
||||||
|
readonly property bool hasArt: artwork.length > 0
|
||||||
|
|
||||||
QQC2.ItemDelegate {
|
QQC2.ItemDelegate {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
anchors.margins: Kirigami.Units.smallSpacing
|
||||||
|
padding: 0
|
||||||
|
|
||||||
readonly property bool isCurrent: GridView.isCurrentItem && grid.activeFocus
|
readonly property bool isCurrent: GridView.isCurrentItem && grid.activeFocus
|
||||||
|
|
||||||
|
|
@ -223,43 +341,99 @@ Window {
|
||||||
Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } }
|
Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } }
|
||||||
}
|
}
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
contentItem: Item {
|
||||||
spacing: Kirigami.Units.smallSpacing
|
// ---- cover art tile ----
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
visible: hasArt
|
||||||
|
|
||||||
Kirigami.Icon {
|
Image {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.fillWidth: true
|
||||||
implicitWidth: Kirigami.Units.iconSizes.huge
|
Layout.fillHeight: true
|
||||||
implicitHeight: Kirigami.Units.iconSizes.huge
|
source: hasArt ? "file://" + artwork : ""
|
||||||
source: icon
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
scale: parent.parent.isCurrent ? 1.08 : 1.0
|
// Rounded top corners via layer
|
||||||
Behavior on scale {
|
layer.enabled: true
|
||||||
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad }
|
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 {
|
// ---- fallback icon tile ----
|
||||||
Layout.alignment: Qt.AlignHCenter
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
anchors.fill: parent
|
||||||
text: name
|
anchors.margins: Kirigami.Units.smallSpacing
|
||||||
maximumLineCount: 2
|
visible: !hasArt
|
||||||
wrapMode: Text.Wrap
|
spacing: Kirigami.Units.smallSpacing
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
elide: Text.ElideRight
|
|
||||||
color: parent.parent.isCurrent
|
|
||||||
? Kirigami.Theme.highlightedTextColor
|
|
||||||
: Kirigami.Theme.textColor
|
|
||||||
}
|
|
||||||
|
|
||||||
// Source badge
|
Item { Layout.fillHeight: true }
|
||||||
PC3.Label {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Kirigami.Icon {
|
||||||
text: source === "steam" ? "Steam"
|
Layout.alignment: Qt.AlignHCenter
|
||||||
: source === "flatpak" ? "Flatpak"
|
implicitWidth: Kirigami.Units.iconSizes.huge
|
||||||
: ""
|
implicitHeight: Kirigami.Units.iconSizes.huge
|
||||||
visible: source !== "desktop"
|
source: icon
|
||||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
|
||||||
opacity: 0.6
|
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