mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-28 14:43:09 +00:00
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.
This commit is contained in:
parent
a8e76002f4
commit
26945e83fa
3 changed files with 127 additions and 18 deletions
|
|
@ -28,6 +28,8 @@
|
|||
#include <memory>
|
||||
|
||||
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<QString> 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;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include <QTimer>
|
||||
#include <qqmlregistration.h>
|
||||
|
||||
#include <KConfigWatcher>
|
||||
#include <KSharedConfig>
|
||||
|
||||
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<GameEntry> m_allGames;
|
||||
QList<GameEntry> 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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
Loading…
Reference in a new issue