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 }