mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-29 15:03: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>
|
#include <memory>
|
||||||
|
|
||||||
static const QString s_recentGroup = QStringLiteral("GamingRecentlyPlayed");
|
static const QString s_recentGroup = QStringLiteral("GamingRecentlyPlayed");
|
||||||
|
static const QString s_waydroidGamingGroup = QStringLiteral("WaydroidGaming");
|
||||||
|
static const QString s_gameShellPackagesKey = QStringLiteral("gameShellPackages");
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
@ -224,6 +226,29 @@ bool parseVdf(const QString &input, VdfNode &root, QString *error)
|
||||||
return false;
|
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
|
} // namespace
|
||||||
|
|
||||||
GameLauncherProvider::GameLauncherProvider(QObject *parent)
|
GameLauncherProvider::GameLauncherProvider(QObject *parent)
|
||||||
|
|
@ -231,6 +256,13 @@ GameLauncherProvider::GameLauncherProvider(QObject *parent)
|
||||||
, m_config(KSharedConfig::openConfig(QStringLiteral("plasmamobilerc")))
|
, m_config(KSharedConfig::openConfig(QStringLiteral("plasmamobilerc")))
|
||||||
{
|
{
|
||||||
connect(KSycoca::self(), &KSycoca::databaseChanged, this, &GameLauncherProvider::refresh);
|
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.setInterval(15000);
|
||||||
m_pendingLaunchTimer.setSingleShot(true);
|
m_pendingLaunchTimer.setSingleShot(true);
|
||||||
connect(&m_pendingLaunchTimer, &QTimer::timeout, this, &GameLauncherProvider::clearPendingLaunch);
|
connect(&m_pendingLaunchTimer, &QTimer::timeout, this, &GameLauncherProvider::clearPendingLaunch);
|
||||||
|
|
@ -363,7 +395,7 @@ void GameLauncherProvider::launchEntry(GameEntry &entry)
|
||||||
{
|
{
|
||||||
clearLastLaunchError();
|
clearLastLaunchError();
|
||||||
|
|
||||||
if (entry.source == QLatin1String("desktop")) {
|
if (entry.source == QLatin1String("desktop") || entry.source == QLatin1String("waydroid")) {
|
||||||
auto service = KService::serviceByStorageId(entry.storageId);
|
auto service = KService::serviceByStorageId(entry.storageId);
|
||||||
if (!service) {
|
if (!service) {
|
||||||
markLaunchFailed(entry.name, QStringLiteral("Desktop entry is no longer available"));
|
markLaunchFailed(entry.name, QStringLiteral("Desktop entry is no longer available"));
|
||||||
|
|
@ -426,6 +458,8 @@ void GameLauncherProvider::deduplicateGames()
|
||||||
|
|
||||||
void GameLauncherProvider::loadDesktopGames()
|
void GameLauncherProvider::loadDesktopGames()
|
||||||
{
|
{
|
||||||
|
const QStringList allowedWaydroidPackages = waydroidGameShellPackages(m_config);
|
||||||
|
const QSet<QString> enabledWaydroidPackages(allowedWaydroidPackages.cbegin(), allowedWaydroidPackages.cend());
|
||||||
const auto services = KService::allServices();
|
const auto services = KService::allServices();
|
||||||
for (const auto &service : services) {
|
for (const auto &service : services) {
|
||||||
if (service->noDisplay() || service->exec().isEmpty()) {
|
if (service->noDisplay() || service->exec().isEmpty()) {
|
||||||
|
|
@ -433,20 +467,29 @@ void GameLauncherProvider::loadDesktopGames()
|
||||||
}
|
}
|
||||||
const QStringList cats = service->categories();
|
const QStringList cats = service->categories();
|
||||||
bool isGame = false;
|
bool isGame = false;
|
||||||
|
bool isWaydroidApp = false;
|
||||||
for (const auto &cat : cats) {
|
for (const auto &cat : cats) {
|
||||||
if (cat.compare(QLatin1String("Game"), Qt::CaseInsensitive) == 0) {
|
if (cat.compare(QLatin1String("Game"), Qt::CaseInsensitive) == 0) {
|
||||||
isGame = true;
|
isGame = true;
|
||||||
break;
|
} else if (cat.compare(QLatin1String("X-WayDroid-App"), Qt::CaseInsensitive) == 0) {
|
||||||
|
isWaydroidApp = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isGame) {
|
if (!isGame) {
|
||||||
|
if (!isWaydroidApp) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QString packageName = waydroidPackageFromService(service);
|
||||||
|
if (packageName.isEmpty() || !enabledWaydroidPackages.contains(packageName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
GameEntry entry;
|
GameEntry entry;
|
||||||
entry.name = service->name();
|
entry.name = service->name();
|
||||||
entry.icon = service->icon();
|
entry.icon = service->icon();
|
||||||
entry.source = QStringLiteral("desktop");
|
entry.source = isWaydroidApp ? QStringLiteral("waydroid") : QStringLiteral("desktop");
|
||||||
entry.storageId = service->storageId();
|
entry.storageId = service->storageId();
|
||||||
entry.launchCommand = service->exec();
|
entry.launchCommand = service->exec();
|
||||||
entry.installed = true;
|
entry.installed = true;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <qqmlregistration.h>
|
#include <qqmlregistration.h>
|
||||||
|
|
||||||
|
#include <KConfigWatcher>
|
||||||
#include <KSharedConfig>
|
#include <KSharedConfig>
|
||||||
|
|
||||||
class GameLauncherProvider : public QAbstractListModel
|
class GameLauncherProvider : public QAbstractListModel
|
||||||
|
|
@ -33,7 +34,7 @@ public:
|
||||||
enum Roles {
|
enum Roles {
|
||||||
NameRole = Qt::UserRole + 1,
|
NameRole = Qt::UserRole + 1,
|
||||||
IconRole,
|
IconRole,
|
||||||
SourceRole, // "desktop", "steam", "flatpak"
|
SourceRole, // "desktop", "waydroid", "steam", "flatpak"
|
||||||
StorageIdRole, // .desktop file name or launch URI
|
StorageIdRole, // .desktop file name or launch URI
|
||||||
LaunchCommandRole,
|
LaunchCommandRole,
|
||||||
ArtworkRole, // path to banner/grid image (empty if none)
|
ArtworkRole, // path to banner/grid image (empty if none)
|
||||||
|
|
@ -107,7 +108,7 @@ private:
|
||||||
QList<GameEntry> m_allGames;
|
QList<GameEntry> m_allGames;
|
||||||
QList<GameEntry> m_games; // filtered view
|
QList<GameEntry> m_games; // filtered view
|
||||||
QString m_filterString;
|
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;
|
KSharedConfigPtr m_config;
|
||||||
bool m_loading = false;
|
bool m_loading = false;
|
||||||
bool m_overlayEnabled = false;
|
bool m_overlayEnabled = false;
|
||||||
|
|
@ -115,4 +116,5 @@ private:
|
||||||
QString m_pendingLaunchName;
|
QString m_pendingLaunchName;
|
||||||
QString m_lastLaunchError;
|
QString m_lastLaunchError;
|
||||||
QTimer m_pendingLaunchTimer;
|
QTimer m_pendingLaunchTimer;
|
||||||
|
KConfigWatcher::Ptr m_configWatcher;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -288,8 +288,8 @@ Window {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cycle through source filter tabs (All → Steam → Desktop → All …)
|
// Cycle through source filter tabs.
|
||||||
readonly property var _sourceFilters: ["", "steam", "desktop", "lutris", "heroic"]
|
readonly property var _sourceFilters: ["", "steam", "desktop", "waydroid", "lutris", "heroic"]
|
||||||
function cycleSourceFilter(direction) {
|
function cycleSourceFilter(direction) {
|
||||||
var current = _sourceFilters.indexOf(
|
var current = _sourceFilters.indexOf(
|
||||||
GamingShell.GameLauncherProvider.sourceFilter)
|
GamingShell.GameLauncherProvider.sourceFilter)
|
||||||
|
|
@ -300,6 +300,40 @@ Window {
|
||||||
sourceFilterBar.currentIndex = next
|
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 {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
Kirigami.Theme.inherit: false
|
Kirigami.Theme.inherit: false
|
||||||
|
|
@ -543,6 +577,11 @@ Window {
|
||||||
width: implicitWidth
|
width: implicitWidth
|
||||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "desktop"
|
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "desktop"
|
||||||
}
|
}
|
||||||
|
QQC2.TabButton {
|
||||||
|
text: i18n("Waydroid")
|
||||||
|
width: implicitWidth
|
||||||
|
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "waydroid"
|
||||||
|
}
|
||||||
QQC2.TabButton {
|
QQC2.TabButton {
|
||||||
text: "Lutris"
|
text: "Lutris"
|
||||||
width: implicitWidth
|
width: implicitWidth
|
||||||
|
|
@ -586,7 +625,7 @@ Window {
|
||||||
: i18n("No games found")
|
: i18n("No games found")
|
||||||
explanation: searchField.text.length > 0
|
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: {
|
onActiveFocusChanged: {
|
||||||
|
|
@ -657,6 +696,26 @@ Window {
|
||||||
smooth: true
|
smooth: true
|
||||||
asynchronous: 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
|
// Title beneath artwork
|
||||||
|
|
@ -710,17 +769,22 @@ Window {
|
||||||
: Kirigami.Theme.textColor
|
: Kirigami.Theme.textColor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Source badge
|
Rectangle {
|
||||||
PC3.Label {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
text: source === "steam" ? "Steam"
|
|
||||||
: source === "flatpak" ? "Flatpak"
|
|
||||||
: source === "lutris" ? "Lutris"
|
|
||||||
: source === "heroic" ? "Heroic"
|
|
||||||
: ""
|
|
||||||
visible: source !== "desktop"
|
visible: source !== "desktop"
|
||||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
radius: height / 2
|
||||||
opacity: 0.6
|
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 }
|
Item { Layout.fillHeight: true }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue