From 26945e83fa33f106de01bad836b36245406a82ea Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Fri, 24 Apr 2026 09:40:53 +0200 Subject: [PATCH] Show Waydroid apps in Game Center Read the Waydroid allowlist from plasmamobilerc and treat matching Waydroid launchers as their own source in the gaming shell. Surface that source in Game Center so Android titles get their own tab, source chip, and empty-state guidance. --- .../gamelauncherprovider.cpp | 51 ++++++++++- .../gamingshellplugin/gamelauncherprovider.h | 6 +- .../folio/qml/gaming/GameCenterOverlay.qml | 88 ++++++++++++++++--- 3 files changed, 127 insertions(+), 18 deletions(-) diff --git a/components/gamingshellplugin/gamelauncherprovider.cpp b/components/gamingshellplugin/gamelauncherprovider.cpp index 3a5dbe99..d37e66cf 100644 --- a/components/gamingshellplugin/gamelauncherprovider.cpp +++ b/components/gamingshellplugin/gamelauncherprovider.cpp @@ -28,6 +28,8 @@ #include static const QString s_recentGroup = QStringLiteral("GamingRecentlyPlayed"); +static const QString s_waydroidGamingGroup = QStringLiteral("WaydroidGaming"); +static const QString s_gameShellPackagesKey = QStringLiteral("gameShellPackages"); namespace { @@ -224,6 +226,29 @@ bool parseVdf(const QString &input, VdfNode &root, QString *error) return false; } } + +QString waydroidPackageFromService(const KService::Ptr &service) +{ + static const QRegularExpression execPattern(QStringLiteral("^waydroid\\s+app\\s+launch\\s+([^\\s%]+)")); + const QRegularExpressionMatch execMatch = execPattern.match(service->exec()); + if (execMatch.hasMatch()) { + return execMatch.captured(1); + } + + static const QRegularExpression storageIdPattern(QStringLiteral("^waydroid\\.(.+)\\.desktop$")); + const QRegularExpressionMatch storageIdMatch = storageIdPattern.match(service->storageId()); + if (!storageIdMatch.hasMatch()) { + return {}; + } + + return storageIdMatch.captured(1); +} + +QStringList waydroidGameShellPackages(const KSharedConfigPtr &config) +{ + const KConfigGroup group(config, s_waydroidGamingGroup); + return group.readEntry(s_gameShellPackagesKey, QStringList{}); +} } // namespace GameLauncherProvider::GameLauncherProvider(QObject *parent) @@ -231,6 +256,13 @@ GameLauncherProvider::GameLauncherProvider(QObject *parent) , m_config(KSharedConfig::openConfig(QStringLiteral("plasmamobilerc"))) { connect(KSycoca::self(), &KSycoca::databaseChanged, this, &GameLauncherProvider::refresh); + m_configWatcher = KConfigWatcher::create(m_config); + connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group) { + if (group.name() == s_waydroidGamingGroup) { + m_config->reparseConfiguration(); + refresh(); + } + }); m_pendingLaunchTimer.setInterval(15000); m_pendingLaunchTimer.setSingleShot(true); connect(&m_pendingLaunchTimer, &QTimer::timeout, this, &GameLauncherProvider::clearPendingLaunch); @@ -363,7 +395,7 @@ void GameLauncherProvider::launchEntry(GameEntry &entry) { clearLastLaunchError(); - if (entry.source == QLatin1String("desktop")) { + if (entry.source == QLatin1String("desktop") || entry.source == QLatin1String("waydroid")) { auto service = KService::serviceByStorageId(entry.storageId); if (!service) { markLaunchFailed(entry.name, QStringLiteral("Desktop entry is no longer available")); @@ -426,6 +458,8 @@ void GameLauncherProvider::deduplicateGames() void GameLauncherProvider::loadDesktopGames() { + const QStringList allowedWaydroidPackages = waydroidGameShellPackages(m_config); + const QSet enabledWaydroidPackages(allowedWaydroidPackages.cbegin(), allowedWaydroidPackages.cend()); const auto services = KService::allServices(); for (const auto &service : services) { if (service->noDisplay() || service->exec().isEmpty()) { @@ -433,20 +467,29 @@ void GameLauncherProvider::loadDesktopGames() } const QStringList cats = service->categories(); bool isGame = false; + bool isWaydroidApp = false; for (const auto &cat : cats) { if (cat.compare(QLatin1String("Game"), Qt::CaseInsensitive) == 0) { isGame = true; - break; + } else if (cat.compare(QLatin1String("X-WayDroid-App"), Qt::CaseInsensitive) == 0) { + isWaydroidApp = true; } } if (!isGame) { - continue; + if (!isWaydroidApp) { + continue; + } + + const QString packageName = waydroidPackageFromService(service); + if (packageName.isEmpty() || !enabledWaydroidPackages.contains(packageName)) { + continue; + } } GameEntry entry; entry.name = service->name(); entry.icon = service->icon(); - entry.source = QStringLiteral("desktop"); + entry.source = isWaydroidApp ? QStringLiteral("waydroid") : QStringLiteral("desktop"); entry.storageId = service->storageId(); entry.launchCommand = service->exec(); entry.installed = true; diff --git a/components/gamingshellplugin/gamelauncherprovider.h b/components/gamingshellplugin/gamelauncherprovider.h index c9d3dd09..675e00f2 100644 --- a/components/gamingshellplugin/gamelauncherprovider.h +++ b/components/gamingshellplugin/gamelauncherprovider.h @@ -10,6 +10,7 @@ #include #include +#include #include class GameLauncherProvider : public QAbstractListModel @@ -33,7 +34,7 @@ public: enum Roles { NameRole = Qt::UserRole + 1, IconRole, - SourceRole, // "desktop", "steam", "flatpak" + SourceRole, // "desktop", "waydroid", "steam", "flatpak" StorageIdRole, // .desktop file name or launch URI LaunchCommandRole, ArtworkRole, // path to banner/grid image (empty if none) @@ -107,7 +108,7 @@ private: QList m_allGames; QList m_games; // filtered view QString m_filterString; - QString m_sourceFilter; // empty = all, or "desktop"/"steam"/"flatpak" + QString m_sourceFilter; // empty = all, or a specific source such as "desktop" or "waydroid" KSharedConfigPtr m_config; bool m_loading = false; bool m_overlayEnabled = false; @@ -115,4 +116,5 @@ private: QString m_pendingLaunchName; QString m_lastLaunchError; QTimer m_pendingLaunchTimer; + KConfigWatcher::Ptr m_configWatcher; }; diff --git a/containments/homescreens/folio/qml/gaming/GameCenterOverlay.qml b/containments/homescreens/folio/qml/gaming/GameCenterOverlay.qml index 8af494af..16fb0e40 100644 --- a/containments/homescreens/folio/qml/gaming/GameCenterOverlay.qml +++ b/containments/homescreens/folio/qml/gaming/GameCenterOverlay.qml @@ -288,8 +288,8 @@ Window { } } - // Cycle through source filter tabs (All → Steam → Desktop → All …) - readonly property var _sourceFilters: ["", "steam", "desktop", "lutris", "heroic"] + // Cycle through source filter tabs. + readonly property var _sourceFilters: ["", "steam", "desktop", "waydroid", "lutris", "heroic"] function cycleSourceFilter(direction) { var current = _sourceFilters.indexOf( GamingShell.GameLauncherProvider.sourceFilter) @@ -300,6 +300,40 @@ Window { sourceFilterBar.currentIndex = next } + function sourceLabel(source) { + switch (source) { + case "steam": + return i18n("Steam") + case "waydroid": + return i18n("Waydroid") + case "lutris": + return i18n("Lutris") + case "heroic": + return i18n("Heroic") + case "flatpak": + return i18n("Flatpak") + default: + return "" + } + } + + function sourceChipColor(source) { + switch (source) { + case "steam": + return Qt.rgba(0.12, 0.23, 0.38, 0.9) + case "waydroid": + return Qt.rgba(0.13, 0.42, 0.36, 0.92) + case "lutris": + return Qt.rgba(0.42, 0.25, 0.11, 0.9) + case "heroic": + return Qt.rgba(0.37, 0.19, 0.16, 0.9) + case "flatpak": + return Qt.rgba(0.16, 0.26, 0.46, 0.9) + default: + return Qt.rgba(0.2, 0.2, 0.2, 0.72) + } + } + Rectangle { anchors.fill: parent Kirigami.Theme.inherit: false @@ -543,6 +577,11 @@ Window { width: implicitWidth onClicked: GamingShell.GameLauncherProvider.sourceFilter = "desktop" } + QQC2.TabButton { + text: i18n("Waydroid") + width: implicitWidth + onClicked: GamingShell.GameLauncherProvider.sourceFilter = "waydroid" + } QQC2.TabButton { text: "Lutris" width: implicitWidth @@ -586,7 +625,7 @@ Window { : i18n("No games found") explanation: searchField.text.length > 0 ? "" - : i18n("Install games or check that they have the Game category in their .desktop file") + : i18n("Install games, or enable supported Waydroid apps from the Waydroid applications page") } onActiveFocusChanged: { @@ -657,6 +696,26 @@ Window { smooth: true asynchronous: true } + + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: Kirigami.Units.smallSpacing + visible: source !== "desktop" + radius: height / 2 + color: root.sourceChipColor(source) + implicitHeight: chipLabel.implicitHeight + Kirigami.Units.smallSpacing + implicitWidth: chipLabel.implicitWidth + Kirigami.Units.largeSpacing + + PC3.Label { + id: chipLabel + anchors.centerIn: parent + text: root.sourceLabel(source) + font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.72 + font.weight: Font.DemiBold + color: "white" + } + } } // Title beneath artwork @@ -710,17 +769,22 @@ Window { : Kirigami.Theme.textColor } - // Source badge - PC3.Label { + Rectangle { Layout.alignment: Qt.AlignHCenter - text: source === "steam" ? "Steam" - : source === "flatpak" ? "Flatpak" - : source === "lutris" ? "Lutris" - : source === "heroic" ? "Heroic" - : "" visible: source !== "desktop" - font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75 - opacity: 0.6 + radius: height / 2 + color: root.sourceChipColor(source) + implicitHeight: sourceChipLabel.implicitHeight + Kirigami.Units.smallSpacing + implicitWidth: sourceChipLabel.implicitWidth + Kirigami.Units.largeSpacing + + PC3.Label { + id: sourceChipLabel + anchors.centerIn: parent + text: root.sourceLabel(source) + font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.72 + font.weight: Font.DemiBold + color: "white" + } } Item { Layout.fillHeight: true }