mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-03-26 17:03:08 +00:00
Compare commits
5 commits
ded8ad83da
...
c7956eed8b
| Author | SHA1 | Date | |
|---|---|---|---|
| c7956eed8b | |||
| 4573a3106e | |||
| 97d915abc6 | |||
| 1f1be64c8f | |||
| d87c227347 |
27 changed files with 3398 additions and 128 deletions
|
|
@ -10,6 +10,7 @@ add_executable(alakarte
|
|||
gamepadmanager.cpp
|
||||
inputmanager.cpp
|
||||
mediamanager.cpp
|
||||
runnermanagerclient.cpp
|
||||
screenshotmodel.cpp
|
||||
platformimporter.cpp
|
||||
steamimporter.cpp
|
||||
|
|
@ -139,3 +140,5 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte
|
|||
install(TARGETS alakarte ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
|
||||
add_subdirectory(gamecenter)
|
||||
|
||||
add_subdirectory(runner)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ App::App(QObject *parent)
|
|||
: QObject(parent)
|
||||
, m_gameModel(new GameModel(this))
|
||||
, m_launcher(new GameLauncher(this))
|
||||
, m_runnerManager(new RunnerManagerClient(this))
|
||||
, m_steamGridDB(new SteamGridDB(this))
|
||||
, m_mediaManager(new MediaManager(this))
|
||||
, m_config(new Config(this))
|
||||
|
|
@ -169,6 +170,11 @@ GameLauncher *App::launcher() const
|
|||
return m_launcher;
|
||||
}
|
||||
|
||||
RunnerManagerClient *App::runnerManager() const
|
||||
{
|
||||
return m_runnerManager;
|
||||
}
|
||||
|
||||
SteamGridDB *App::steamGridDB() const
|
||||
{
|
||||
return m_steamGridDB;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include "gamelauncher.h"
|
||||
#include "gamemodel.h"
|
||||
#include "mediamanager.h"
|
||||
#include "runnermanagerclient.h"
|
||||
#include "steamgriddb.h"
|
||||
|
||||
class App : public QObject
|
||||
|
|
@ -22,6 +23,7 @@ class App : public QObject
|
|||
|
||||
Q_PROPERTY(GameModel *gameModel READ gameModel CONSTANT)
|
||||
Q_PROPERTY(GameLauncher *launcher READ launcher CONSTANT)
|
||||
Q_PROPERTY(RunnerManagerClient *runnerManager READ runnerManager CONSTANT)
|
||||
Q_PROPERTY(SteamGridDB *steamGridDB READ steamGridDB CONSTANT)
|
||||
Q_PROPERTY(MediaManager *mediaManager READ mediaManager CONSTANT)
|
||||
Q_PROPERTY(Config *config READ config CONSTANT)
|
||||
|
|
@ -34,6 +36,7 @@ public:
|
|||
|
||||
GameModel *gameModel() const;
|
||||
GameLauncher *launcher() const;
|
||||
RunnerManagerClient *runnerManager() const;
|
||||
SteamGridDB *steamGridDB() const;
|
||||
MediaManager *mediaManager() const;
|
||||
Config *config() const;
|
||||
|
|
@ -74,6 +77,7 @@ private:
|
|||
|
||||
GameModel *m_gameModel;
|
||||
GameLauncher *m_launcher;
|
||||
RunnerManagerClient *m_runnerManager;
|
||||
SteamGridDB *m_steamGridDB;
|
||||
MediaManager *m_mediaManager;
|
||||
Config *m_config;
|
||||
|
|
|
|||
27
src/game.cpp
27
src/game.cpp
|
|
@ -159,6 +159,19 @@ void Game::setLaunchRunner(const QString &runner)
|
|||
}
|
||||
}
|
||||
|
||||
QString Game::launchRunnerId() const
|
||||
{
|
||||
return m_launchRunnerId;
|
||||
}
|
||||
|
||||
void Game::setLaunchRunnerId(const QString &runnerId)
|
||||
{
|
||||
if (m_launchRunnerId != runnerId) {
|
||||
m_launchRunnerId = runnerId;
|
||||
Q_EMIT launchRunnerIdChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QString Game::launchRunnerPath() const
|
||||
{
|
||||
return m_launchRunnerPath;
|
||||
|
|
@ -323,7 +336,8 @@ QJsonObject Game::toJson() const
|
|||
obj[QStringLiteral("hidden")] = m_hidden;
|
||||
obj[QStringLiteral("installed")] = m_installed;
|
||||
|
||||
const bool hasLaunchConfig = !m_launchEnv.isEmpty() || !m_launchRunner.isEmpty() || !m_launchRunnerPath.isEmpty() || !m_launchPrefixPath.isEmpty();
|
||||
const bool hasLaunchConfig =
|
||||
!m_launchEnv.isEmpty() || !m_launchRunner.isEmpty() || !m_launchRunnerId.isEmpty() || !m_launchRunnerPath.isEmpty() || !m_launchPrefixPath.isEmpty();
|
||||
if (hasLaunchConfig) {
|
||||
QJsonObject launchObj;
|
||||
|
||||
|
|
@ -338,6 +352,9 @@ QJsonObject Game::toJson() const
|
|||
if (!m_launchRunner.isEmpty()) {
|
||||
launchObj.insert(QStringLiteral("runner"), m_launchRunner);
|
||||
}
|
||||
if (!m_launchRunnerId.isEmpty()) {
|
||||
launchObj.insert(QStringLiteral("runnerId"), m_launchRunnerId);
|
||||
}
|
||||
if (!m_launchRunnerPath.isEmpty()) {
|
||||
launchObj.insert(QStringLiteral("runnerPath"), m_launchRunnerPath);
|
||||
}
|
||||
|
|
@ -391,6 +408,7 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
|
|||
|
||||
QVariantMap env;
|
||||
QString runner;
|
||||
QString runnerId;
|
||||
QString runnerPath;
|
||||
QString prefixPath;
|
||||
const QJsonValue launchValue = json.value(QStringLiteral("launch"));
|
||||
|
|
@ -402,6 +420,7 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
|
|||
}
|
||||
|
||||
runner = launchObj.value(QStringLiteral("runner")).toString();
|
||||
runnerId = launchObj.value(QStringLiteral("runnerId")).toString();
|
||||
runnerPath = launchObj.value(QStringLiteral("runnerPath")).toString();
|
||||
prefixPath = launchObj.value(QStringLiteral("prefixPath")).toString();
|
||||
}
|
||||
|
|
@ -414,6 +433,9 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
|
|||
if (runner.isEmpty()) {
|
||||
runner = json.value(QStringLiteral("launchRunner")).toString();
|
||||
}
|
||||
if (runnerId.isEmpty()) {
|
||||
runnerId = json.value(QStringLiteral("launchRunnerId")).toString();
|
||||
}
|
||||
if (runnerPath.isEmpty()) {
|
||||
runnerPath = json.value(QStringLiteral("launchRunnerPath")).toString();
|
||||
}
|
||||
|
|
@ -428,6 +450,9 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
|
|||
if (!runner.isEmpty()) {
|
||||
game->setLaunchRunner(runner);
|
||||
}
|
||||
if (!runnerId.isEmpty()) {
|
||||
game->setLaunchRunnerId(runnerId);
|
||||
}
|
||||
if (!runnerPath.isEmpty()) {
|
||||
game->setLaunchRunnerPath(runnerPath);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class Game : public QObject
|
|||
Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged)
|
||||
Q_PROPERTY(QVariantMap launchEnv READ launchEnv WRITE setLaunchEnv NOTIFY launchEnvChanged)
|
||||
Q_PROPERTY(QString launchRunner READ launchRunner WRITE setLaunchRunner NOTIFY launchRunnerChanged)
|
||||
Q_PROPERTY(QString launchRunnerId READ launchRunnerId WRITE setLaunchRunnerId NOTIFY launchRunnerIdChanged)
|
||||
Q_PROPERTY(QString launchRunnerPath READ launchRunnerPath WRITE setLaunchRunnerPath NOTIFY launchRunnerPathChanged)
|
||||
Q_PROPERTY(QString launchPrefixPath READ launchPrefixPath WRITE setLaunchPrefixPath NOTIFY launchPrefixPathChanged)
|
||||
Q_PROPERTY(QString platform READ platform WRITE setPlatform NOTIFY platformChanged)
|
||||
|
|
@ -76,6 +77,9 @@ public:
|
|||
QString launchRunner() const;
|
||||
void setLaunchRunner(const QString &runner);
|
||||
|
||||
QString launchRunnerId() const;
|
||||
void setLaunchRunnerId(const QString &runnerId);
|
||||
|
||||
QString launchRunnerPath() const;
|
||||
void setLaunchRunnerPath(const QString &path);
|
||||
|
||||
|
|
@ -127,6 +131,7 @@ Q_SIGNALS:
|
|||
void workingDirectoryChanged();
|
||||
void launchEnvChanged();
|
||||
void launchRunnerChanged();
|
||||
void launchRunnerIdChanged();
|
||||
void launchRunnerPathChanged();
|
||||
void launchPrefixPathChanged();
|
||||
void platformChanged();
|
||||
|
|
@ -151,6 +156,7 @@ private:
|
|||
QString m_workingDirectory;
|
||||
QVariantMap m_launchEnv;
|
||||
QString m_launchRunner;
|
||||
QString m_launchRunnerId;
|
||||
QString m_launchRunnerPath;
|
||||
QString m_launchPrefixPath;
|
||||
QString m_platform;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ install(TARGETS alakarte_gamecenter ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
|||
|
||||
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus")
|
||||
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd")
|
||||
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus-system")
|
||||
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd-system")
|
||||
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus-policy")
|
||||
|
||||
configure_file(dbus/org.kde.GameCenter1.service.in
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
|
||||
|
|
@ -35,6 +38,21 @@ configure_file(systemd/org.kde.GameCenter1.service.in
|
|||
@ONLY
|
||||
)
|
||||
|
||||
configure_file(dbus/org.kde.GameCenter1.system.service.in
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/dbus-system/org.kde.GameCenter1.service"
|
||||
@ONLY
|
||||
)
|
||||
|
||||
configure_file(systemd/org.kde.GameCenter1.system.service.in
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/systemd-system/org.kde.GameCenter1.service"
|
||||
@ONLY
|
||||
)
|
||||
|
||||
configure_file(dbus/org.kde.GameCenter1.conf.in
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/dbus-policy/org.kde.GameCenter1.conf"
|
||||
@ONLY
|
||||
)
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
|
||||
DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}
|
||||
)
|
||||
|
|
@ -42,3 +60,15 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
|
|||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.GameCenter1.service"
|
||||
DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}
|
||||
)
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus-system/org.kde.GameCenter1.service"
|
||||
DESTINATION ${KDE_INSTALL_DBUSSYSTEMSERVICEDIR}
|
||||
)
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd-system/org.kde.GameCenter1.service"
|
||||
DESTINATION ${KDE_INSTALL_SYSTEMDUNITDIR}
|
||||
)
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus-policy/org.kde.GameCenter1.conf"
|
||||
DESTINATION ${KDE_INSTALL_SYSCONFDIR}/dbus-1/system.d
|
||||
)
|
||||
|
|
|
|||
13
src/gamecenter/dbus/org.kde.GameCenter1.conf.in
Normal file
13
src/gamecenter/dbus/org.kde.GameCenter1.conf.in
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE busconfig PUBLIC
|
||||
"-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
||||
<busconfig>
|
||||
<policy user="root">
|
||||
<allow own="org.kde.GameCenter1"/>
|
||||
</policy>
|
||||
|
||||
<policy context="default">
|
||||
<allow send_destination="org.kde.GameCenter1"/>
|
||||
<allow receive_sender="org.kde.GameCenter1"/>
|
||||
</policy>
|
||||
</busconfig>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
[D-BUS Service]
|
||||
Name=org.kde.GameCenter1
|
||||
Exec=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-gamecenter --system
|
||||
SystemdService=org.kde.GameCenter1.service
|
||||
|
|
@ -7,9 +7,13 @@
|
|||
#include <QDBusConnection>
|
||||
#include <QDBusConnectionInterface>
|
||||
#include <QDBusError>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusMessage>
|
||||
#include <QDBusObjectPath>
|
||||
#include <QDBusReply>
|
||||
#include <QDBusVariant>
|
||||
#include <QDBusVirtualObject>
|
||||
#include <QDir>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QStandardPaths>
|
||||
|
|
@ -19,8 +23,14 @@
|
|||
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <QSet>
|
||||
|
||||
#include <utility>
|
||||
|
||||
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
|
||||
static const QString kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1");
|
||||
static const QString kRunnerInterface = QStringLiteral("org.kde.ALaKarte.Runner1");
|
||||
|
||||
static QString ensureScopeUnitName(const QString &unitName)
|
||||
{
|
||||
if (unitName.endsWith(QLatin1String(".scope"))) {
|
||||
|
|
@ -49,6 +59,10 @@ static QVariant normalizeForDbus(QVariant v)
|
|||
return QString(v.toChar());
|
||||
}
|
||||
|
||||
if (v.userType() == QMetaType::QString || v.userType() == QMetaType::QStringList) {
|
||||
return v;
|
||||
}
|
||||
|
||||
if (v.canConvert<QDBusArgument>()) {
|
||||
const QDBusArgument arg = v.value<QDBusArgument>();
|
||||
const QMap<QString, QString> asStringMap = qdbus_cast<QMap<QString, QString>>(arg);
|
||||
|
|
@ -77,7 +91,7 @@ static QVariant normalizeForDbus(QVariant v)
|
|||
}
|
||||
return m;
|
||||
}
|
||||
if (v.canConvert<QVariantList>()) {
|
||||
if (v.userType() == QMetaType::QVariantList) {
|
||||
QVariantList list = v.toList();
|
||||
for (QVariant &item : list) {
|
||||
item = normalizeForDbus(item);
|
||||
|
|
@ -96,6 +110,57 @@ static QVariantMap normalizeVariantMapForDbus(const QVariantMap &map)
|
|||
return out;
|
||||
}
|
||||
|
||||
static bool tryResolveWithRunnerManager(const QVariantMap &spec, QVariantMap &out)
|
||||
{
|
||||
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||
if (!bus.isConnected() || !bus.interface()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!bus.interface()->isServiceRegistered(kRunnerService)) {
|
||||
bus.interface()->startService(kRunnerService);
|
||||
}
|
||||
|
||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
|
||||
if (!iface.isValid()) {
|
||||
if (bus.interface()->startService(kRunnerService).isValid()) {
|
||||
QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
|
||||
if (retryIface.isValid()) {
|
||||
retryIface.setTimeout(2000);
|
||||
const QDBusReply<QVariantMap> retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec);
|
||||
if (retryReply.isValid()) {
|
||||
out = normalizeVariantMapForDbus(retryReply.value());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
iface.setTimeout(2000);
|
||||
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ResolveLaunch"), spec);
|
||||
if (!reply.isValid()) {
|
||||
if (reply.error().type() == QDBusError::ServiceUnknown) {
|
||||
bus.interface()->startService(kRunnerService);
|
||||
QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
|
||||
if (!retryIface.isValid()) {
|
||||
return false;
|
||||
}
|
||||
retryIface.setTimeout(2000);
|
||||
const QDBusReply<QVariantMap> retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec);
|
||||
if (!retryReply.isValid()) {
|
||||
return false;
|
||||
}
|
||||
out = normalizeVariantMapForDbus(retryReply.value());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
out = normalizeVariantMapForDbus(reply.value());
|
||||
return true;
|
||||
}
|
||||
|
||||
static QString mapStringValue(const QVariantMap &map, const QStringList &keys)
|
||||
{
|
||||
for (const QString &k : keys) {
|
||||
|
|
@ -171,6 +236,445 @@ private:
|
|||
|
||||
} // namespace
|
||||
|
||||
static const QString kGameCenterService = QStringLiteral("org.kde.GameCenter1");
|
||||
static const QString kGameCenterPath = QStringLiteral("/org/kde/GameCenter1");
|
||||
static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1");
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
static QString gameCenterIntrospectionXml()
|
||||
{
|
||||
return QStringLiteral(
|
||||
"<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-Bus Object Introspection 1.0//EN\" "
|
||||
"\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">"
|
||||
"<node>"
|
||||
" <interface name=\"org.freedesktop.DBus.Introspectable\">"
|
||||
" <method name=\"Introspect\">"
|
||||
" <arg name=\"xml\" type=\"s\" direction=\"out\"/>"
|
||||
" </method>"
|
||||
" </interface>"
|
||||
" <interface name=\"org.kde.GameCenter1\">"
|
||||
" <method name=\"Ping\">"
|
||||
" <arg name=\"out\" type=\"s\" direction=\"out\"/>"
|
||||
" </method>"
|
||||
" <method name=\"Version\">"
|
||||
" <arg name=\"major\" type=\"u\" direction=\"out\"/>"
|
||||
" <arg name=\"minor\" type=\"u\" direction=\"out\"/>"
|
||||
" </method>"
|
||||
" <method name=\"GetCapabilities\">"
|
||||
" <arg name=\"caps\" type=\"a{sv}\" direction=\"out\"/>"
|
||||
" </method>"
|
||||
" <method name=\"ListSessions\">"
|
||||
" <arg name=\"sessions\" type=\"av\" direction=\"out\"/>"
|
||||
" </method>"
|
||||
" <method name=\"GetSession\">"
|
||||
" <arg name=\"sessionId\" type=\"s\" direction=\"in\"/>"
|
||||
" <arg name=\"session\" type=\"a{sv}\" direction=\"out\"/>"
|
||||
" </method>"
|
||||
" <method name=\"Launch\">"
|
||||
" <arg name=\"launchSpec\" type=\"a{sv}\" direction=\"in\"/>"
|
||||
" <arg name=\"sessionId\" type=\"s\" direction=\"out\"/>"
|
||||
" </method>"
|
||||
" <method name=\"Stop\">"
|
||||
" <arg name=\"sessionId\" type=\"s\" direction=\"in\"/>"
|
||||
" </method>"
|
||||
" <method name=\"StopByGameId\">"
|
||||
" <arg name=\"gameId\" type=\"s\" direction=\"in\"/>"
|
||||
" </method>"
|
||||
" <method name=\"SetPolicy\">"
|
||||
" <arg name=\"policy\" type=\"a{sv}\" direction=\"in\"/>"
|
||||
" </method>"
|
||||
" <method name=\"GetPolicy\">"
|
||||
" <arg name=\"policy\" type=\"a{sv}\" direction=\"out\"/>"
|
||||
" </method>"
|
||||
" <signal name=\"SessionAdded\">"
|
||||
" <arg name=\"session\" type=\"a{sv}\"/>"
|
||||
" </signal>"
|
||||
" <signal name=\"SessionChanged\">"
|
||||
" <arg name=\"session\" type=\"a{sv}\"/>"
|
||||
" </signal>"
|
||||
" <signal name=\"SessionRemoved\">"
|
||||
" <arg name=\"sessionId\" type=\"s\"/>"
|
||||
" <arg name=\"finalState\" type=\"a{sv}\"/>"
|
||||
" </signal>"
|
||||
" <signal name=\"LaunchFailed\">"
|
||||
" <arg name=\"error\" type=\"a{sv}\"/>"
|
||||
" </signal>"
|
||||
" </interface>"
|
||||
"</node>");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class GameCenterSystemProxyPrivate;
|
||||
|
||||
class GameCenterProxyVirtualObject : public QDBusVirtualObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GameCenterProxyVirtualObject(GameCenterSystemProxyPrivate *proxy, QObject *parent = nullptr)
|
||||
: QDBusVirtualObject(parent)
|
||||
, m_proxy(proxy)
|
||||
{
|
||||
}
|
||||
|
||||
QString introspect(const QString &path) const override;
|
||||
bool handleMessage(const QDBusMessage &message, const QDBusConnection &connection) override;
|
||||
|
||||
private:
|
||||
GameCenterSystemProxyPrivate *m_proxy = nullptr;
|
||||
};
|
||||
|
||||
class BackendSignalForwarder : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit BackendSignalForwarder(uint uid, GameCenterSystemProxyPrivate *proxy, QObject *parent = nullptr)
|
||||
: QObject(parent)
|
||||
, m_uid(uid)
|
||||
, m_proxy(proxy)
|
||||
{
|
||||
}
|
||||
|
||||
public Q_SLOTS:
|
||||
void onSessionAdded(const QVariantMap &session);
|
||||
void onSessionChanged(const QVariantMap &session);
|
||||
void onSessionRemoved(const QString &sessionId, const QVariantMap &finalState);
|
||||
void onLaunchFailed(const QVariantMap &error);
|
||||
|
||||
private:
|
||||
uint m_uid = 0;
|
||||
GameCenterSystemProxyPrivate *m_proxy = nullptr;
|
||||
};
|
||||
|
||||
struct BackendBusEntry {
|
||||
BackendBusEntry(const QDBusConnection &bus, BackendSignalForwarder *forwarder)
|
||||
: bus(bus)
|
||||
, forwarder(forwarder)
|
||||
{
|
||||
}
|
||||
|
||||
QDBusConnection bus;
|
||||
BackendSignalForwarder *forwarder = nullptr;
|
||||
};
|
||||
|
||||
class GameCenterSystemProxyPrivate : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GameCenterSystemProxyPrivate(QObject *parent = nullptr)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool init();
|
||||
|
||||
bool handleSystemMessage(const QDBusMessage &message, const QDBusConnection &connection);
|
||||
void forwardSignalToUid(uint uid, const QString &member, const QList<QVariant> &args);
|
||||
|
||||
private Q_SLOTS:
|
||||
void handleServiceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner);
|
||||
|
||||
private:
|
||||
bool uidForSystemSender(const QString &senderUniqueName, uint &uid) const;
|
||||
void rememberClient(const QString &senderUniqueName, uint uid);
|
||||
void forgetClient(const QString &senderUniqueName);
|
||||
BackendBusEntry *ensureBackend(uint uid);
|
||||
|
||||
QDBusConnection m_systemBus = QDBusConnection::systemBus();
|
||||
std::unique_ptr<GameCenterProxyVirtualObject> m_virtualObject;
|
||||
|
||||
QHash<uint, BackendBusEntry> m_backends;
|
||||
QHash<QString, uint> m_clientUid;
|
||||
QHash<uint, QSet<QString>> m_uidClients;
|
||||
};
|
||||
|
||||
QString GameCenterProxyVirtualObject::introspect(const QString &path) const
|
||||
{
|
||||
if (path == kGameCenterPath) {
|
||||
return gameCenterIntrospectionXml();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool GameCenterProxyVirtualObject::handleMessage(const QDBusMessage &message, const QDBusConnection &connection)
|
||||
{
|
||||
if (!m_proxy) {
|
||||
return false;
|
||||
}
|
||||
return m_proxy->handleSystemMessage(message, connection);
|
||||
}
|
||||
|
||||
void BackendSignalForwarder::onSessionAdded(const QVariantMap &session)
|
||||
{
|
||||
if (m_proxy) {
|
||||
m_proxy->forwardSignalToUid(m_uid, QStringLiteral("SessionAdded"), {session});
|
||||
}
|
||||
}
|
||||
|
||||
void BackendSignalForwarder::onSessionChanged(const QVariantMap &session)
|
||||
{
|
||||
if (m_proxy) {
|
||||
m_proxy->forwardSignalToUid(m_uid, QStringLiteral("SessionChanged"), {session});
|
||||
}
|
||||
}
|
||||
|
||||
void BackendSignalForwarder::onSessionRemoved(const QString &sessionId, const QVariantMap &finalState)
|
||||
{
|
||||
if (m_proxy) {
|
||||
m_proxy->forwardSignalToUid(m_uid, QStringLiteral("SessionRemoved"), {sessionId, finalState});
|
||||
}
|
||||
}
|
||||
|
||||
void BackendSignalForwarder::onLaunchFailed(const QVariantMap &error)
|
||||
{
|
||||
if (m_proxy) {
|
||||
m_proxy->forwardSignalToUid(m_uid, QStringLiteral("LaunchFailed"), {error});
|
||||
}
|
||||
}
|
||||
|
||||
bool GameCenterSystemProxyPrivate::init()
|
||||
{
|
||||
if (!m_systemBus.isConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_systemBus.registerService(kGameCenterService)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_virtualObject = std::make_unique<GameCenterProxyVirtualObject>(this);
|
||||
if (!m_systemBus.registerVirtualObject(kGameCenterPath, m_virtualObject.get(), QDBusConnection::SingleNode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_systemBus.interface()) {
|
||||
connect(m_systemBus.interface(), &QDBusConnectionInterface::serviceOwnerChanged, this, &GameCenterSystemProxyPrivate::handleServiceOwnerChanged);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GameCenterSystemProxyPrivate::uidForSystemSender(const QString &senderUniqueName, uint &uid) const
|
||||
{
|
||||
if (!m_systemBus.interface()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QDBusReply<uint> uidReply = m_systemBus.interface()->serviceUid(senderUniqueName);
|
||||
if (!uidReply.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uid = uidReply.value();
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameCenterSystemProxyPrivate::rememberClient(const QString &senderUniqueName, uint uid)
|
||||
{
|
||||
const auto it = m_clientUid.constFind(senderUniqueName);
|
||||
if (it != m_clientUid.constEnd()) {
|
||||
if (it.value() == uid) {
|
||||
return;
|
||||
}
|
||||
forgetClient(senderUniqueName);
|
||||
}
|
||||
|
||||
m_clientUid.insert(senderUniqueName, uid);
|
||||
m_uidClients[uid].insert(senderUniqueName);
|
||||
}
|
||||
|
||||
void GameCenterSystemProxyPrivate::forgetClient(const QString &senderUniqueName)
|
||||
{
|
||||
const auto it = m_clientUid.find(senderUniqueName);
|
||||
if (it == m_clientUid.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint uid = it.value();
|
||||
m_clientUid.erase(it);
|
||||
|
||||
auto clientsIt = m_uidClients.find(uid);
|
||||
if (clientsIt != m_uidClients.end()) {
|
||||
clientsIt.value().remove(senderUniqueName);
|
||||
if (clientsIt.value().isEmpty()) {
|
||||
m_uidClients.erase(clientsIt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameCenterSystemProxyPrivate::handleServiceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner)
|
||||
{
|
||||
Q_UNUSED(oldOwner)
|
||||
|
||||
if (!newOwner.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name.startsWith(QLatin1Char(':'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
forgetClient(name);
|
||||
}
|
||||
|
||||
BackendBusEntry *GameCenterSystemProxyPrivate::ensureBackend(uint uid)
|
||||
{
|
||||
const auto it = m_backends.find(uid);
|
||||
if (it != m_backends.end()) {
|
||||
return &it.value();
|
||||
}
|
||||
|
||||
const QString address = QStringLiteral("unix:path=/run/user/%1/bus").arg(uid);
|
||||
const QString connectionName = QStringLiteral("gamecenter-user-%1").arg(uid);
|
||||
QDBusConnection backend = QDBusConnection::connectToBus(address, connectionName);
|
||||
if (!backend.isConnected()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto *forwarder = new BackendSignalForwarder(uid, this, this);
|
||||
BackendBusEntry entry(backend, forwarder);
|
||||
|
||||
backend.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("SessionAdded"), forwarder, SLOT(onSessionAdded(QVariantMap)));
|
||||
|
||||
backend
|
||||
.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("SessionChanged"), forwarder, SLOT(onSessionChanged(QVariantMap)));
|
||||
|
||||
backend.connect(kGameCenterService,
|
||||
kGameCenterPath,
|
||||
kGameCenterInterface,
|
||||
QStringLiteral("SessionRemoved"),
|
||||
forwarder,
|
||||
SLOT(onSessionRemoved(QString, QVariantMap)));
|
||||
|
||||
backend.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("LaunchFailed"), forwarder, SLOT(onLaunchFailed(QVariantMap)));
|
||||
|
||||
auto inserted = m_backends.insert(uid, entry);
|
||||
return &inserted.value();
|
||||
}
|
||||
|
||||
void GameCenterSystemProxyPrivate::forwardSignalToUid(uint uid, const QString &member, const QList<QVariant> &args)
|
||||
{
|
||||
if (!m_systemBus.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto clientsIt = m_uidClients.constFind(uid);
|
||||
if (clientsIt == m_uidClients.constEnd()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QSet<QString> clients = clientsIt.value();
|
||||
for (const QString &client : clients) {
|
||||
if (client.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QDBusMessage signal = QDBusMessage::createTargetedSignal(client, kGameCenterPath, kGameCenterInterface, member);
|
||||
signal.setArguments(args);
|
||||
m_systemBus.send(signal);
|
||||
}
|
||||
}
|
||||
|
||||
bool GameCenterSystemProxyPrivate::handleSystemMessage(const QDBusMessage &message, const QDBusConnection &connection)
|
||||
{
|
||||
if (message.type() != QDBusMessage::MethodCallMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.interface() == QLatin1String("org.freedesktop.DBus.Introspectable") && message.member() == QLatin1String("Introspect")) {
|
||||
if (!message.isReplyRequired()) {
|
||||
return true;
|
||||
}
|
||||
connection.send(message.createReply(gameCenterIntrospectionXml()));
|
||||
return true;
|
||||
}
|
||||
|
||||
const QString sender = message.service();
|
||||
if (sender.isEmpty()) {
|
||||
if (message.isReplyRequired()) {
|
||||
connection.send(message.createErrorReply(QDBusError::AccessDenied, QStringLiteral("missing sender")));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint uid = 0;
|
||||
if (!uidForSystemSender(sender, uid)) {
|
||||
if (message.isReplyRequired()) {
|
||||
connection.send(message.createErrorReply(QDBusError::AccessDenied, QStringLiteral("failed to determine caller uid")));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
rememberClient(sender, uid);
|
||||
|
||||
BackendBusEntry *backend = ensureBackend(uid);
|
||||
if (!backend) {
|
||||
if (message.isReplyRequired()) {
|
||||
connection.send(message.createErrorReply(QDBusError::ServiceUnknown, QStringLiteral("user bus backend unavailable")));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QDBusMessage forward = QDBusMessage::createMethodCall(kGameCenterService, message.path(), message.interface(), message.member());
|
||||
forward.setArguments(message.arguments());
|
||||
forward.setAutoStartService(true);
|
||||
|
||||
const QDBusMessage backendReply = backend->bus.call(forward, QDBus::Block, -1);
|
||||
if (!message.isReplyRequired()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (backendReply.type() == QDBusMessage::ErrorMessage) {
|
||||
connection.send(message.createErrorReply(backendReply.errorName(), backendReply.errorMessage()));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (backendReply.type() != QDBusMessage::ReplyMessage) {
|
||||
connection.send(message.createErrorReply(QDBusError::Failed, QStringLiteral("backend did not reply")));
|
||||
return true;
|
||||
}
|
||||
|
||||
QDBusMessage out = message.createReply(backendReply.arguments());
|
||||
if (message.interface() == kGameCenterInterface && message.member() == QLatin1String("GetCapabilities")) {
|
||||
if (!out.arguments().isEmpty()) {
|
||||
QVariant capsV = unwrapDbusVariant(out.arguments().first());
|
||||
QVariantMap caps;
|
||||
if (capsV.canConvert<QDBusArgument>()) {
|
||||
const QDBusArgument arg = capsV.value<QDBusArgument>();
|
||||
caps = qdbus_cast<QVariantMap>(arg);
|
||||
} else if (capsV.canConvert<QVariantMap>()) {
|
||||
caps = capsV.toMap();
|
||||
}
|
||||
|
||||
if (!caps.isEmpty()) {
|
||||
caps.insert(QStringLiteral("supportsSystemBus"), true);
|
||||
out.setArguments({caps});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connection.send(out);
|
||||
return true;
|
||||
}
|
||||
|
||||
GameCenterSystemProxy::GameCenterSystemProxy(QObject *parent)
|
||||
: QObject(parent)
|
||||
, d(std::make_unique<GameCenterSystemProxyPrivate>())
|
||||
{
|
||||
}
|
||||
|
||||
GameCenterSystemProxy::~GameCenterSystemProxy() = default;
|
||||
|
||||
bool GameCenterSystemProxy::init()
|
||||
{
|
||||
return d->init();
|
||||
}
|
||||
|
||||
void GameCenterDaemon::watchSystemdUnit(const QString &sessionId, const QString &unitName, const QDBusObjectPath &unitPath)
|
||||
{
|
||||
if (unitName.isEmpty()) {
|
||||
|
|
@ -545,6 +1049,69 @@ QString GameCenterDaemon::launchDirect(const QVariantMap &launchSpec)
|
|||
envOverrides = extractVariantMap(launchSpec.value(QStringLiteral("envOverrides")));
|
||||
}
|
||||
|
||||
const QString runnerId = mapStringValue(launchSpec, {QStringLiteral("runnerId")});
|
||||
const QString runner = mapStringValue(launchSpec, {QStringLiteral("runner")});
|
||||
const QString runnerPath = mapStringValue(launchSpec, {QStringLiteral("runnerPath")});
|
||||
const QString prefixPath = mapStringValue(launchSpec, {QStringLiteral("prefixPath")});
|
||||
|
||||
const QString requestedProgram = mapStringValue(launchSpec, {QStringLiteral("requestedProgram")});
|
||||
QStringList requestedArgs;
|
||||
if (launchSpec.contains(QStringLiteral("requestedArgs"))) {
|
||||
requestedArgs = variantToStringList(launchSpec.value(QStringLiteral("requestedArgs")));
|
||||
}
|
||||
|
||||
QVariantMap requestedEnvOverrides;
|
||||
if (launchSpec.contains(QStringLiteral("requestedEnvOverrides"))) {
|
||||
requestedEnvOverrides = extractVariantMap(launchSpec.value(QStringLiteral("requestedEnvOverrides")));
|
||||
}
|
||||
|
||||
const bool wantsRunnerResolution = !runnerId.isEmpty() || runner == QLatin1String("wine") || runner == QLatin1String("proton");
|
||||
if (wantsRunnerResolution) {
|
||||
const QString baseProgram = requestedProgram.isEmpty() ? program : requestedProgram;
|
||||
const QStringList baseArgs = requestedArgs.isEmpty() ? args : requestedArgs;
|
||||
const QVariantMap baseEnv = requestedEnvOverrides.isEmpty() ? envOverrides : requestedEnvOverrides;
|
||||
|
||||
const QVariantMap resolveSpec = {
|
||||
{QStringLiteral("runnerId"), runnerId},
|
||||
{QStringLiteral("runner"), runner},
|
||||
{QStringLiteral("runnerPath"), runnerPath},
|
||||
{QStringLiteral("gameId"), gameId},
|
||||
{QStringLiteral("prefixPath"), prefixPath},
|
||||
{QStringLiteral("program"), baseProgram},
|
||||
{QStringLiteral("args"), baseArgs},
|
||||
{QStringLiteral("envOverrides"), baseEnv},
|
||||
};
|
||||
|
||||
QVariantMap resolved;
|
||||
if (tryResolveWithRunnerManager(resolveSpec, resolved)) {
|
||||
if (!resolved.value(QStringLiteral("ok")).toBool()) {
|
||||
QVariantMap ctx = launchSpec;
|
||||
ctx.insert(QStringLiteral("runnerResolution"), resolved);
|
||||
failLaunch(resolved.value(QStringLiteral("error")).toString(), ctx);
|
||||
return {};
|
||||
}
|
||||
|
||||
program = mapStringValue(resolved, {QStringLiteral("finalProgram")});
|
||||
if (resolved.contains(QStringLiteral("finalArgs"))) {
|
||||
args = variantToStringList(resolved.value(QStringLiteral("finalArgs")));
|
||||
}
|
||||
if (resolved.contains(QStringLiteral("effectiveEnv"))) {
|
||||
envOverrides = extractVariantMap(resolved.value(QStringLiteral("effectiveEnv")));
|
||||
}
|
||||
|
||||
const QString resolvedPrefixPath = mapStringValue(resolved, {QStringLiteral("resolvedPrefixPath")});
|
||||
if (!resolvedPrefixPath.isEmpty()) {
|
||||
QDir().mkpath(resolvedPrefixPath);
|
||||
}
|
||||
} else if (!runnerId.isEmpty()) {
|
||||
QVariantMap ctx = launchSpec;
|
||||
ctx.insert(QStringLiteral("runnerResolution"),
|
||||
QVariantMap{{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("runner service unavailable")}});
|
||||
failLaunch(QStringLiteral("runner service is not available"), ctx);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const QString sessionId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
const QString unitName = ensureScopeUnitName(QStringLiteral("alakarte-game-%1").arg(sessionId));
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QDBusObjectPath>
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
|
|
@ -94,3 +96,19 @@ private:
|
|||
QHash<QString, QString> m_unitNameToSessionId;
|
||||
QHash<QString, QObject *> m_unitPathWatchers;
|
||||
};
|
||||
|
||||
class GameCenterSystemProxyPrivate;
|
||||
|
||||
class GameCenterSystemProxy : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GameCenterSystemProxy(QObject *parent = nullptr);
|
||||
~GameCenterSystemProxy() override;
|
||||
|
||||
bool init();
|
||||
|
||||
private:
|
||||
std::unique_ptr<GameCenterSystemProxyPrivate> d;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||
|
||||
#include <QCommandLineOption>
|
||||
#include <QCommandLineParser>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include "gamecenterdaemon.h"
|
||||
|
|
@ -9,6 +11,21 @@ int main(int argc, char *argv[])
|
|||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription(QStringLiteral("A-La-Karte Game Center"));
|
||||
parser.addHelpOption();
|
||||
parser.addOption(QCommandLineOption(QStringLiteral("system"), QStringLiteral("Run as system bus proxy")));
|
||||
parser.process(app);
|
||||
|
||||
if (parser.isSet(QStringLiteral("system"))) {
|
||||
GameCenterSystemProxy proxy;
|
||||
if (!proxy.init()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
GameCenterDaemon daemon;
|
||||
if (!daemon.init()) {
|
||||
return 1;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
[Unit]
|
||||
Description=A-La-Karte Game Center
|
||||
|
||||
[Service]
|
||||
ExecStart=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-gamecenter --system
|
||||
Type=dbus
|
||||
BusName=org.kde.GameCenter1
|
||||
Restart=no
|
||||
|
|
@ -7,6 +7,8 @@
|
|||
#include <QCoreApplication>
|
||||
#include <QDBusArgument>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusConnectionInterface>
|
||||
#include <QDBusError>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusReply>
|
||||
#include <QDBusVariant>
|
||||
|
|
@ -19,16 +21,88 @@
|
|||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
static QStringList steamCandidateRoots()
|
||||
static const QString kGameCenterService = QStringLiteral("org.kde.GameCenter1");
|
||||
static const QString kGameCenterPath = QStringLiteral("/org/kde/GameCenter1");
|
||||
static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1");
|
||||
|
||||
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
|
||||
static const QString kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1");
|
||||
static const QString kRunnerInterface = QStringLiteral("org.kde.ALaKarte.Runner1");
|
||||
|
||||
static bool pingDaemon(QDBusConnection bus)
|
||||
{
|
||||
const QString home = QDir::homePath();
|
||||
return {
|
||||
home + QStringLiteral("/.steam/root"),
|
||||
home + QStringLiteral("/.steam/steam"),
|
||||
home + QStringLiteral("/.local/share/Steam"),
|
||||
home + QStringLiteral("/.var/app/com.valvesoftware.Steam/data/Steam"),
|
||||
home + QStringLiteral("/.var/app/com.valvesoftware.Steam/.local/share/Steam"),
|
||||
};
|
||||
if (!bus.isConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QDBusInterface iface(kGameCenterService, kGameCenterPath, kGameCenterInterface, bus);
|
||||
if (!iface.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
iface.setTimeout(2000);
|
||||
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
|
||||
return reply.isValid() && reply.value() == QLatin1String("ok");
|
||||
}
|
||||
|
||||
static void disconnectDaemonSignals(QDBusConnection bus, GameLauncher *launcher)
|
||||
{
|
||||
if (!bus.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bus.disconnect(kGameCenterService,
|
||||
kGameCenterPath,
|
||||
kGameCenterInterface,
|
||||
QStringLiteral("SessionAdded"),
|
||||
launcher,
|
||||
SLOT(onDaemonSessionAdded(QVariantMap)));
|
||||
|
||||
bus.disconnect(kGameCenterService,
|
||||
kGameCenterPath,
|
||||
kGameCenterInterface,
|
||||
QStringLiteral("SessionChanged"),
|
||||
launcher,
|
||||
SLOT(onDaemonSessionChanged(QVariantMap)));
|
||||
|
||||
bus.disconnect(kGameCenterService,
|
||||
kGameCenterPath,
|
||||
kGameCenterInterface,
|
||||
QStringLiteral("SessionRemoved"),
|
||||
launcher,
|
||||
SLOT(onDaemonSessionRemoved(QString, QVariantMap)));
|
||||
|
||||
bus.disconnect(kGameCenterService,
|
||||
kGameCenterPath,
|
||||
kGameCenterInterface,
|
||||
QStringLiteral("LaunchFailed"),
|
||||
launcher,
|
||||
SLOT(onDaemonLaunchFailed(QVariantMap)));
|
||||
}
|
||||
|
||||
static void connectDaemonSignals(QDBusConnection bus, GameLauncher *launcher)
|
||||
{
|
||||
if (!bus.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bus.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("SessionAdded"), launcher, SLOT(onDaemonSessionAdded(QVariantMap)));
|
||||
|
||||
bus.connect(kGameCenterService,
|
||||
kGameCenterPath,
|
||||
kGameCenterInterface,
|
||||
QStringLiteral("SessionChanged"),
|
||||
launcher,
|
||||
SLOT(onDaemonSessionChanged(QVariantMap)));
|
||||
|
||||
bus.connect(kGameCenterService,
|
||||
kGameCenterPath,
|
||||
kGameCenterInterface,
|
||||
QStringLiteral("SessionRemoved"),
|
||||
launcher,
|
||||
SLOT(onDaemonSessionRemoved(QString, QVariantMap)));
|
||||
|
||||
bus.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("LaunchFailed"), launcher, SLOT(onDaemonLaunchFailed(QVariantMap)));
|
||||
}
|
||||
|
||||
static QVariant unwrapDbusVariant(QVariant v)
|
||||
|
|
@ -48,6 +122,59 @@ static QVariantMap unwrapVariantMap(const QVariantMap &map)
|
|||
return out;
|
||||
}
|
||||
|
||||
static bool tryResolveWithRunnerManager(const QVariantMap &spec, QVariantMap &out)
|
||||
{
|
||||
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||
if (!bus.isConnected() || !bus.interface()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!bus.interface()->isServiceRegistered(kRunnerService)) {
|
||||
bus.interface()->startService(kRunnerService);
|
||||
}
|
||||
|
||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
|
||||
if (!iface.isValid()) {
|
||||
if (bus.interface()->startService(kRunnerService).isValid()) {
|
||||
QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
|
||||
if (retryIface.isValid()) {
|
||||
retryIface.setTimeout(2000);
|
||||
const QDBusReply<QVariantMap> retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec);
|
||||
if (retryReply.isValid()) {
|
||||
out = unwrapVariantMap(retryReply.value());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
iface.setTimeout(2000);
|
||||
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ResolveLaunch"), spec);
|
||||
if (!reply.isValid()) {
|
||||
if (reply.error().type() == QDBusError::ServiceUnknown) {
|
||||
bus.interface()->startService(kRunnerService);
|
||||
QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
|
||||
if (!retryIface.isValid()) {
|
||||
return false;
|
||||
}
|
||||
retryIface.setTimeout(2000);
|
||||
const QDBusReply<QVariantMap> retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec);
|
||||
if (!retryReply.isValid()) {
|
||||
return false;
|
||||
}
|
||||
out = unwrapVariantMap(retryReply.value());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
out = unwrapVariantMap(reply.value());
|
||||
return true;
|
||||
}
|
||||
|
||||
static QStringList steamCandidateRoots();
|
||||
|
||||
static QString findSteamClientInstallPathFromProton(const QString &protonExe)
|
||||
{
|
||||
if (protonExe.isEmpty()) {
|
||||
|
|
@ -130,40 +257,23 @@ static QString discoverDefaultProtonExecutable()
|
|||
return cached;
|
||||
}
|
||||
|
||||
static QStringList steamCandidateRoots()
|
||||
{
|
||||
const QString home = QDir::homePath();
|
||||
return {
|
||||
home + QStringLiteral("/.steam/root"),
|
||||
home + QStringLiteral("/.steam/steam"),
|
||||
home + QStringLiteral("/.local/share/Steam"),
|
||||
home + QStringLiteral("/.var/app/com.valvesoftware.Steam/data/Steam"),
|
||||
home + QStringLiteral("/.var/app/com.valvesoftware.Steam/.local/share/Steam"),
|
||||
};
|
||||
}
|
||||
|
||||
GameLauncher::GameLauncher(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_daemonAvailable(false)
|
||||
, m_usingSystemBus(false)
|
||||
{
|
||||
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||
if (bus.isConnected()) {
|
||||
bus.connect(QStringLiteral("org.kde.GameCenter1"),
|
||||
QStringLiteral("/org/kde/GameCenter1"),
|
||||
QStringLiteral("org.kde.GameCenter1"),
|
||||
QStringLiteral("SessionAdded"),
|
||||
this,
|
||||
SLOT(onDaemonSessionAdded(QVariantMap)));
|
||||
|
||||
bus.connect(QStringLiteral("org.kde.GameCenter1"),
|
||||
QStringLiteral("/org/kde/GameCenter1"),
|
||||
QStringLiteral("org.kde.GameCenter1"),
|
||||
QStringLiteral("SessionChanged"),
|
||||
this,
|
||||
SLOT(onDaemonSessionChanged(QVariantMap)));
|
||||
|
||||
bus.connect(QStringLiteral("org.kde.GameCenter1"),
|
||||
QStringLiteral("/org/kde/GameCenter1"),
|
||||
QStringLiteral("org.kde.GameCenter1"),
|
||||
QStringLiteral("SessionRemoved"),
|
||||
this,
|
||||
SLOT(onDaemonSessionRemoved(QString, QVariantMap)));
|
||||
|
||||
bus.connect(QStringLiteral("org.kde.GameCenter1"),
|
||||
QStringLiteral("/org/kde/GameCenter1"),
|
||||
QStringLiteral("org.kde.GameCenter1"),
|
||||
QStringLiteral("LaunchFailed"),
|
||||
this,
|
||||
SLOT(onDaemonLaunchFailed(QVariantMap)));
|
||||
}
|
||||
|
||||
if (auto *app = qobject_cast<App *>(parent)) {
|
||||
connect(app->gameModel(), &GameModel::countChanged, this, [this]() {
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
|
|
@ -222,12 +332,14 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
|
|||
}
|
||||
|
||||
const QString runner = game->launchRunner().trimmed();
|
||||
const QString runnerId = game->launchRunnerId().trimmed();
|
||||
const QString runnerPath = game->launchRunnerPath().trimmed();
|
||||
const QString prefixPath = game->launchPrefixPath().trimmed();
|
||||
const QVariantMap launchEnv = game->launchEnv();
|
||||
const bool hasLaunchOverrides = !runner.isEmpty() || !runnerPath.isEmpty() || !prefixPath.isEmpty() || !launchEnv.isEmpty();
|
||||
const bool hasLaunchOverrides = !runner.isEmpty() || !runnerId.isEmpty() || !runnerPath.isEmpty() || !prefixPath.isEmpty() || !launchEnv.isEmpty();
|
||||
|
||||
info.insert(QStringLiteral("runner"), runner);
|
||||
info.insert(QStringLiteral("runnerId"), runnerId);
|
||||
info.insert(QStringLiteral("runnerPath"), runnerPath);
|
||||
info.insert(QStringLiteral("prefixPath"), prefixPath);
|
||||
info.insert(QStringLiteral("workingDirectory"), game->workingDirectory());
|
||||
|
|
@ -287,6 +399,47 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
|
|||
return info;
|
||||
}
|
||||
|
||||
if (!runnerId.isEmpty() || runner == QLatin1String("wine") || runner == QLatin1String("proton")) {
|
||||
QVariantMap runnerSpec = {
|
||||
{QStringLiteral("runnerId"), runnerId},
|
||||
{QStringLiteral("runner"), runner},
|
||||
{QStringLiteral("runnerPath"), runnerPath},
|
||||
{QStringLiteral("gameId"), game->id()},
|
||||
{QStringLiteral("prefixPath"), prefixPath},
|
||||
{QStringLiteral("program"), program},
|
||||
{QStringLiteral("args"), parts},
|
||||
{QStringLiteral("envOverrides"), envOverrides},
|
||||
};
|
||||
|
||||
QVariantMap resolved;
|
||||
if (tryResolveWithRunnerManager(runnerSpec, resolved)) {
|
||||
if (!resolved.value(QStringLiteral("ok")).toBool()) {
|
||||
info.insert(QStringLiteral("error"), resolved.value(QStringLiteral("error")).toString());
|
||||
return info;
|
||||
}
|
||||
|
||||
info.insert(QStringLiteral("finalProgram"), resolved.value(QStringLiteral("finalProgram")).toString());
|
||||
info.insert(QStringLiteral("finalArgs"), resolved.value(QStringLiteral("finalArgs")).toStringList());
|
||||
info.insert(QStringLiteral("effectiveEnv"), resolved.value(QStringLiteral("effectiveEnv")).toMap());
|
||||
|
||||
const QString resolvedPrefixPath = resolved.value(QStringLiteral("resolvedPrefixPath")).toString();
|
||||
if (!resolvedPrefixPath.isEmpty()) {
|
||||
info.insert(QStringLiteral("resolvedPrefixPath"), resolvedPrefixPath);
|
||||
}
|
||||
|
||||
const QString resolvedSteamInstallPath = resolved.value(QStringLiteral("resolvedSteamInstallPath")).toString();
|
||||
if (!resolvedSteamInstallPath.isEmpty()) {
|
||||
info.insert(QStringLiteral("resolvedSteamInstallPath"), resolvedSteamInstallPath);
|
||||
}
|
||||
|
||||
info.insert(QStringLiteral("ok"), true);
|
||||
return info;
|
||||
} else if (!runnerId.isEmpty()) {
|
||||
info.insert(QStringLiteral("error"), tr("Runner service is not available"));
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
if (runner == QLatin1String("custom")) {
|
||||
if (runnerPath.isEmpty()) {
|
||||
info.insert(QStringLiteral("error"), tr("Custom runner is enabled but no runner path is configured"));
|
||||
|
|
@ -354,7 +507,6 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
|
|||
}
|
||||
|
||||
void GameLauncher::launchGame(Game *game)
|
||||
|
||||
{
|
||||
if (!game) {
|
||||
return;
|
||||
|
|
@ -376,7 +528,12 @@ void GameLauncher::launchGame(Game *game)
|
|||
const QString launchType = info.value(QStringLiteral("launchType")).toString();
|
||||
const QString provider = info.value(QStringLiteral("provider")).toString();
|
||||
|
||||
if (info.value(QStringLiteral("runner")).toString() == QLatin1String("proton")) {
|
||||
const QString runner = info.value(QStringLiteral("runner")).toString();
|
||||
const QString runnerId = info.value(QStringLiteral("runnerId")).toString();
|
||||
const QString runnerPath = info.value(QStringLiteral("runnerPath")).toString();
|
||||
const QString prefixPath = info.value(QStringLiteral("prefixPath")).toString();
|
||||
const QVariantMap envOverrides = info.value(QStringLiteral("envOverrides")).toMap();
|
||||
if (runner == QLatin1String("proton") || runner == QLatin1String("wine")) {
|
||||
const QString resolvedPrefixPath = info.value(QStringLiteral("resolvedPrefixPath")).toString();
|
||||
if (!resolvedPrefixPath.isEmpty()) {
|
||||
QDir().mkpath(resolvedPrefixPath);
|
||||
|
|
@ -390,10 +547,10 @@ void GameLauncher::launchGame(Game *game)
|
|||
|
||||
// Always try daemon first — for all launch types
|
||||
{
|
||||
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
|
||||
QStringLiteral("/org/kde/GameCenter1"),
|
||||
QStringLiteral("org.kde.GameCenter1"),
|
||||
QDBusConnection::sessionBus());
|
||||
QDBusInterface iface(kGameCenterService,
|
||||
kGameCenterPath,
|
||||
kGameCenterInterface,
|
||||
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus());
|
||||
QVariantMap launchSpec = {
|
||||
{QStringLiteral("command"), launchCommand},
|
||||
{QStringLiteral("gameId"), game->id()},
|
||||
|
|
@ -401,6 +558,26 @@ void GameLauncher::launchGame(Game *game)
|
|||
{QStringLiteral("provider"), provider},
|
||||
{QStringLiteral("origin"), QStringLiteral("ui")},
|
||||
};
|
||||
|
||||
if (!runnerId.isEmpty()) {
|
||||
launchSpec.insert(QStringLiteral("runnerId"), runnerId);
|
||||
}
|
||||
if (!runner.isEmpty()) {
|
||||
launchSpec.insert(QStringLiteral("runner"), runner);
|
||||
}
|
||||
if (!runnerPath.isEmpty()) {
|
||||
launchSpec.insert(QStringLiteral("runnerPath"), runnerPath);
|
||||
}
|
||||
if (!prefixPath.isEmpty()) {
|
||||
launchSpec.insert(QStringLiteral("prefixPath"), prefixPath);
|
||||
}
|
||||
|
||||
launchSpec.insert(QStringLiteral("requestedProgram"), info.value(QStringLiteral("program")).toString());
|
||||
launchSpec.insert(QStringLiteral("requestedArgs"), info.value(QStringLiteral("args")).toStringList());
|
||||
if (!envOverrides.isEmpty()) {
|
||||
launchSpec.insert(QStringLiteral("requestedEnvOverrides"), envOverrides);
|
||||
}
|
||||
|
||||
if (!finalProgram.isEmpty()) {
|
||||
launchSpec.insert(QStringLiteral("program"), finalProgram);
|
||||
launchSpec.insert(QStringLiteral("args"), finalArgs);
|
||||
|
|
@ -427,6 +604,8 @@ void GameLauncher::launchGame(Game *game)
|
|||
return;
|
||||
}
|
||||
|
||||
const QString launchError = reply.isValid() ? QString() : reply.error().message();
|
||||
|
||||
// Daemon call failed — fallback for URL-type commands only
|
||||
if (launchType == QLatin1String("url")) {
|
||||
const QString url = info.value(QStringLiteral("url")).toString();
|
||||
|
|
@ -439,7 +618,11 @@ void GameLauncher::launchGame(Game *game)
|
|||
}
|
||||
|
||||
// No fallback for non-URL commands — emit error
|
||||
Q_EMIT gameError(game, tr("Game Center daemon is not available"));
|
||||
if (!launchError.isEmpty()) {
|
||||
Q_EMIT gameError(game, tr("Game Center launch failed: %1").arg(launchError));
|
||||
} else {
|
||||
Q_EMIT gameError(game, tr("Game Center daemon is not available"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -449,10 +632,10 @@ void GameLauncher::stopGame(Game *game)
|
|||
return;
|
||||
}
|
||||
|
||||
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
|
||||
QStringLiteral("/org/kde/GameCenter1"),
|
||||
QStringLiteral("org.kde.GameCenter1"),
|
||||
QDBusConnection::sessionBus());
|
||||
QDBusInterface iface(kGameCenterService,
|
||||
kGameCenterPath,
|
||||
kGameCenterInterface,
|
||||
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus());
|
||||
iface.call(QStringLiteral("StopByGameId"), game->id());
|
||||
}
|
||||
|
||||
|
|
@ -583,10 +766,10 @@ void GameLauncher::onDaemonLaunchFailed(const QVariantMap &error)
|
|||
|
||||
void GameLauncher::syncDaemonSessions()
|
||||
{
|
||||
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
|
||||
QStringLiteral("/org/kde/GameCenter1"),
|
||||
QStringLiteral("org.kde.GameCenter1"),
|
||||
QDBusConnection::sessionBus());
|
||||
QDBusInterface iface(kGameCenterService,
|
||||
kGameCenterPath,
|
||||
kGameCenterInterface,
|
||||
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus());
|
||||
|
||||
const QDBusReply<QVariantList> reply = iface.call(QStringLiteral("ListSessions"));
|
||||
if (!reply.isValid()) {
|
||||
|
|
@ -623,13 +806,28 @@ void GameLauncher::applyRunningStateToLibrary()
|
|||
|
||||
void GameLauncher::checkDaemonAvailability()
|
||||
{
|
||||
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
|
||||
QStringLiteral("/org/kde/GameCenter1"),
|
||||
QStringLiteral("org.kde.GameCenter1"),
|
||||
QDBusConnection::sessionBus());
|
||||
const bool systemAvailable = pingDaemon(QDBusConnection::systemBus());
|
||||
const bool sessionAvailable = systemAvailable ? false : pingDaemon(QDBusConnection::sessionBus());
|
||||
const bool available = systemAvailable || sessionAvailable;
|
||||
const bool useSystemBus = systemAvailable;
|
||||
|
||||
disconnectDaemonSignals(QDBusConnection::systemBus(), this);
|
||||
disconnectDaemonSignals(QDBusConnection::sessionBus(), this);
|
||||
|
||||
if (available) {
|
||||
connectDaemonSignals(useSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus(), this);
|
||||
}
|
||||
|
||||
const bool busChanged = m_usingSystemBus != useSystemBus;
|
||||
if (busChanged) {
|
||||
m_daemonGameToSession.clear();
|
||||
m_daemonSessionToGame.clear();
|
||||
Q_EMIT runningGamesChanged();
|
||||
applyRunningStateToLibrary();
|
||||
}
|
||||
|
||||
m_usingSystemBus = useSystemBus;
|
||||
|
||||
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
|
||||
const bool available = reply.isValid() && reply.value() == QLatin1String("ok");
|
||||
if (available != m_daemonAvailable) {
|
||||
m_daemonAvailable = available;
|
||||
Q_EMIT daemonAvailableChanged();
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ private:
|
|||
QHash<QString, QString> m_daemonGameToSession;
|
||||
QHash<QString, QString> m_daemonSessionToGame;
|
||||
bool m_daemonAvailable = false;
|
||||
bool m_usingSystemBus = false;
|
||||
|
||||
void checkDaemonAvailability();
|
||||
void syncDaemonSessions();
|
||||
|
|
|
|||
|
|
@ -17,6 +17,26 @@
|
|||
|
||||
K_PLUGIN_CLASS_WITH_JSON(AlakarteRunner, "plasma-runner-alakarte.json")
|
||||
|
||||
static const QString kGameCenterService = QStringLiteral("org.kde.GameCenter1");
|
||||
static const QString kGameCenterPath = QStringLiteral("/org/kde/GameCenter1");
|
||||
static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1");
|
||||
|
||||
static bool launchViaDaemon(QDBusConnection bus, const QVariantMap &launchSpec)
|
||||
{
|
||||
if (!bus.isConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QDBusInterface iface(kGameCenterService, kGameCenterPath, kGameCenterInterface, bus);
|
||||
if (!iface.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
iface.setTimeout(2000);
|
||||
const QDBusReply<QString> reply = iface.call(QStringLiteral("Launch"), launchSpec);
|
||||
return reply.isValid() && !reply.value().isEmpty();
|
||||
}
|
||||
|
||||
AlakarteRunner::AlakarteRunner(QObject *parent, const KPluginMetaData &metaData)
|
||||
: KRunner::AbstractRunner(parent, metaData)
|
||||
{
|
||||
|
|
@ -154,10 +174,6 @@ void AlakarteRunner::run(const KRunner::RunnerContext &context, const KRunner::Q
|
|||
|
||||
// Always try daemon first for all commands (including Steam/Lutris)
|
||||
if (!match.id().isEmpty()) {
|
||||
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
|
||||
QStringLiteral("/org/kde/GameCenter1"),
|
||||
QStringLiteral("org.kde.GameCenter1"),
|
||||
QDBusConnection::sessionBus());
|
||||
QVariantMap launchSpec = {
|
||||
{QStringLiteral("command"), command},
|
||||
{QStringLiteral("gameId"), match.id()},
|
||||
|
|
@ -165,8 +181,7 @@ void AlakarteRunner::run(const KRunner::RunnerContext &context, const KRunner::Q
|
|||
{QStringLiteral("origin"), QStringLiteral("krunner")},
|
||||
};
|
||||
|
||||
const QDBusReply<QString> reply = iface.call(QStringLiteral("Launch"), launchSpec);
|
||||
if (reply.isValid() && !reply.value().isEmpty()) {
|
||||
if (launchViaDaemon(QDBusConnection::systemBus(), launchSpec) || launchViaDaemon(QDBusConnection::sessionBus(), launchSpec)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,18 @@ Kirigami.OverlaySheet {
|
|||
wrapMode: Text.WrapAnywhere
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Kirigami.FormData.label: i18n("Runner ID:")
|
||||
text: launchInfo && launchInfo.runnerId ? launchInfo.runnerId : i18n("None")
|
||||
wrapMode: Text.WrapAnywhere
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Kirigami.FormData.label: i18n("Runner path:")
|
||||
text: launchInfo && launchInfo.runnerPath ? launchInfo.runnerPath : i18n("Not specified")
|
||||
wrapMode: Text.WrapAnywhere
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Kirigami.FormData.label: i18n("Program:")
|
||||
text: launchInfo && launchInfo.finalProgram ? launchInfo.finalProgram : ""
|
||||
|
|
@ -120,6 +132,12 @@ Kirigami.OverlaySheet {
|
|||
text: launchInfo && launchInfo.resolvedPrefixPath ? launchInfo.resolvedPrefixPath : (launchInfo && launchInfo.prefixPath ? launchInfo.prefixPath : "")
|
||||
wrapMode: Text.WrapAnywhere
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Kirigami.FormData.label: i18n("Steam install path:")
|
||||
text: launchInfo && launchInfo.resolvedSteamInstallPath ? launchInfo.resolvedSteamInstallPath : i18n("Not applicable")
|
||||
wrapMode: Text.WrapAnywhere
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ Kirigami.Dialog {
|
|||
id: envModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: runnerChoicesModel
|
||||
}
|
||||
|
||||
signal gameCreated(var game)
|
||||
signal gameUpdated(var game)
|
||||
|
||||
|
|
@ -35,14 +39,35 @@ Kirigami.Dialog {
|
|||
Kirigami.Action {
|
||||
text: isEditing ? i18n("Apply") : i18n("Add")
|
||||
icon.name: isEditing ? "dialog-ok-apply" : "list-add"
|
||||
enabled: nameField.text.trim() !== "" && executableField.text.trim() !== "" && (runnerCombo.currentIndex !== 3 || runnerPathField.text.trim() !== "")
|
||||
enabled: nameField.text.trim() !== "" && executableField.text.trim() !== "" && (dialog.currentRunnerChoiceKind() !== "custom" || runnerPathField.text.trim() !== "")
|
||||
onTriggered: {
|
||||
let runnerValue = dialog.runnerFromIndex(runnerCombo.currentIndex)
|
||||
let choice = dialog.currentRunnerChoice()
|
||||
let runnerKind = choice && choice.kind ? String(choice.kind) : "native"
|
||||
let runnerType = choice && choice.runnerType ? String(choice.runnerType) : (choice && choice.runner ? String(choice.runner) : "")
|
||||
|
||||
let runnerValue = choice && choice.runner ? String(choice.runner) : ""
|
||||
let runnerIdValue = choice && choice.runnerId ? String(choice.runnerId) : ""
|
||||
|
||||
let runnerPathValue = runnerPathField.text.trim()
|
||||
let prefixPathValue = prefixPathField.text.trim()
|
||||
if (runnerValue === "") {
|
||||
|
||||
if (runnerKind === "native") {
|
||||
runnerValue = ""
|
||||
runnerIdValue = ""
|
||||
runnerPathValue = ""
|
||||
prefixPathValue = ""
|
||||
} else if (runnerKind === "inventory") {
|
||||
runnerValue = ""
|
||||
runnerPathValue = ""
|
||||
} else if (runnerKind === "wine" || runnerKind === "proton") {
|
||||
runnerIdValue = ""
|
||||
} else if (runnerKind === "custom") {
|
||||
runnerIdValue = ""
|
||||
prefixPathValue = ""
|
||||
}
|
||||
|
||||
if (runnerType !== "wine" && runnerType !== "proton") {
|
||||
prefixPathValue = ""
|
||||
}
|
||||
if (isEditing) {
|
||||
game.name = nameField.text.trim()
|
||||
|
|
@ -51,6 +76,7 @@ Kirigami.Dialog {
|
|||
game.workingDirectory = workingDirField.text.trim()
|
||||
game.launchEnv = dialog.envModelToMap()
|
||||
game.launchRunner = runnerValue
|
||||
game.launchRunnerId = runnerIdValue
|
||||
game.launchRunnerPath = runnerPathValue
|
||||
game.launchPrefixPath = prefixPathValue
|
||||
if (selectedCoverPath !== "") {
|
||||
|
|
@ -65,6 +91,7 @@ Kirigami.Dialog {
|
|||
newGame.workingDirectory = workingDirField.text.trim()
|
||||
newGame.launchEnv = dialog.envModelToMap()
|
||||
newGame.launchRunner = runnerValue
|
||||
newGame.launchRunnerId = runnerIdValue
|
||||
newGame.launchRunnerPath = runnerPathValue
|
||||
newGame.launchPrefixPath = prefixPathValue
|
||||
if (selectedCoverPath !== "") {
|
||||
|
|
@ -86,7 +113,24 @@ Kirigami.Dialog {
|
|||
|
||||
property string selectedCoverPath: ""
|
||||
|
||||
readonly property bool anyMenuOpen: runnerCombo && runnerCombo.popup && runnerCombo.popup.visible
|
||||
readonly property bool anyConfirmOpen: !!(deletePrefixConfirmDialog && deletePrefixConfirmDialog.opened)
|
||||
|
||||
readonly property bool anyMenuOpen: !!(runnerCombo && runnerCombo.popup && runnerCombo.popup.visible)
|
||||
|
||||
function currentConfirmDialog() {
|
||||
if (deletePrefixConfirmDialog && deletePrefixConfirmDialog.opened) return deletePrefixConfirmDialog
|
||||
return null
|
||||
}
|
||||
|
||||
function closeCurrentConfirmDialog() {
|
||||
let d = currentConfirmDialog()
|
||||
if (!d) return
|
||||
if (typeof d.reject === "function") {
|
||||
d.reject()
|
||||
} else {
|
||||
d.close()
|
||||
}
|
||||
}
|
||||
|
||||
function closeCurrentMenu() {
|
||||
if (runnerCombo && runnerCombo.popup && runnerCombo.popup.visible) {
|
||||
|
|
@ -181,16 +225,84 @@ Kirigami.Dialog {
|
|||
return 0
|
||||
}
|
||||
|
||||
function currentRunnerChoice() {
|
||||
if (!runnerChoicesModel || runnerChoicesModel.count <= 0) return null
|
||||
let idx = runnerCombo ? runnerCombo.currentIndex : 0
|
||||
if (idx < 0 || idx >= runnerChoicesModel.count) return runnerChoicesModel.get(0)
|
||||
return runnerChoicesModel.get(idx)
|
||||
}
|
||||
|
||||
function currentRunnerChoiceKind() {
|
||||
let c = dialog.currentRunnerChoice()
|
||||
if (!c || !c.kind) return "native"
|
||||
return String(c.kind)
|
||||
}
|
||||
|
||||
function currentRunnerChoiceRunnerType() {
|
||||
let c = dialog.currentRunnerChoice()
|
||||
if (!c) return ""
|
||||
if (c.runnerType) return String(c.runnerType)
|
||||
if (c.runner) return String(c.runner)
|
||||
return ""
|
||||
}
|
||||
|
||||
function rebuildRunnerChoices() {
|
||||
runnerChoicesModel.clear()
|
||||
|
||||
runnerChoicesModel.append({ kind: "native", label: i18n("Native"), runner: "", runnerId: "", runnerType: "" })
|
||||
runnerChoicesModel.append({ kind: "wine", label: i18n("Wine (system)"), runner: "wine", runnerId: "", runnerType: "wine" })
|
||||
runnerChoicesModel.append({ kind: "proton", label: i18n("Proton (auto)"), runner: "proton", runnerId: "", runnerType: "proton" })
|
||||
|
||||
let runners = (App.runnerManager && App.runnerManager.runners) ? App.runnerManager.runners : []
|
||||
for (let i = 0; i < runners.length; i++) {
|
||||
let r = runners[i]
|
||||
if (!r || !r.id) continue
|
||||
let name = r.name ? String(r.name) : String(r.id)
|
||||
let source = r.source ? String(r.source) : ""
|
||||
let type = r.type ? String(r.type) : ""
|
||||
let label = name
|
||||
if (type !== "" || source !== "") {
|
||||
label = name + " (" + type + (source !== "" ? (" · " + source) : "") + ")"
|
||||
}
|
||||
runnerChoicesModel.append({ kind: "inventory", label: label, runner: "", runnerId: String(r.id), runnerType: type })
|
||||
}
|
||||
|
||||
runnerChoicesModel.append({ kind: "custom", label: i18n("Custom runner path"), runner: "custom", runnerId: "", runnerType: "custom" })
|
||||
}
|
||||
|
||||
function indexForRunnerId(runnerId) {
|
||||
let id = (runnerId || "").trim()
|
||||
if (id === "") return 0
|
||||
for (let i = 0; i < runnerChoicesModel.count; i++) {
|
||||
let row = runnerChoicesModel.get(i)
|
||||
if (row.kind === "inventory" && String(row.runnerId || "") === id) return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function indexForRunner(runner) {
|
||||
let r = (runner || "").trim().toLowerCase()
|
||||
if (r === "wine") return 1
|
||||
if (r === "proton") return 2
|
||||
if (r === "custom") return Math.max(0, runnerChoicesModel.count - 1)
|
||||
return 0
|
||||
}
|
||||
|
||||
function loadFields() {
|
||||
selectedCoverPath = ""
|
||||
envModel.clear()
|
||||
dialog.rebuildRunnerChoices()
|
||||
if (isEditing && game) {
|
||||
nameField.text = game.name || ""
|
||||
developerField.text = game.developer || ""
|
||||
executableField.text = game.launchCommand || ""
|
||||
workingDirField.text = game.workingDirectory || ""
|
||||
|
||||
runnerCombo.currentIndex = dialog.runnerToIndex(game.launchRunner)
|
||||
let idx = dialog.indexForRunnerId(game.launchRunnerId)
|
||||
if (idx === 0) {
|
||||
idx = dialog.indexForRunner(game.launchRunner)
|
||||
}
|
||||
runnerCombo.currentIndex = idx
|
||||
runnerPathField.text = game.launchRunnerPath || ""
|
||||
prefixPathField.text = game.launchPrefixPath || ""
|
||||
|
||||
|
|
@ -213,6 +325,66 @@ Kirigami.Dialog {
|
|||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: App.runnerManager
|
||||
function onRunnersChanged() {
|
||||
if (!dialog.opened) return
|
||||
|
||||
let desiredRunnerId = ""
|
||||
if (isEditing && game) {
|
||||
desiredRunnerId = String(game.launchRunnerId || "").trim()
|
||||
}
|
||||
|
||||
let current = dialog.currentRunnerChoice()
|
||||
let currentRunnerId = current && current.runnerId ? String(current.runnerId) : ""
|
||||
let currentRunner = current && current.runner ? String(current.runner) : ""
|
||||
|
||||
dialog.rebuildRunnerChoices()
|
||||
|
||||
if (desiredRunnerId !== "") {
|
||||
runnerCombo.currentIndex = dialog.indexForRunnerId(desiredRunnerId)
|
||||
} else if (currentRunnerId !== "") {
|
||||
runnerCombo.currentIndex = dialog.indexForRunnerId(currentRunnerId)
|
||||
} else {
|
||||
runnerCombo.currentIndex = dialog.indexForRunner(currentRunner)
|
||||
}
|
||||
}
|
||||
|
||||
function onPrefixEnsured(gameId, prefixPath) {
|
||||
if (!dialog.opened) return
|
||||
if (!isEditing || !game) return
|
||||
if (String(gameId) !== String(game.id)) return
|
||||
prefixPathField.text = String(prefixPath || "")
|
||||
}
|
||||
|
||||
function onPrefixDeleted(gameId, prefixPath) {
|
||||
if (!dialog.opened) return
|
||||
if (!isEditing || !game) return
|
||||
if (String(gameId) !== String(game.id)) return
|
||||
prefixPathField.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
function effectivePrefixPath() {
|
||||
let p = prefixPathField.text.trim()
|
||||
if (p !== "") return p
|
||||
if (!isEditing || !game) return ""
|
||||
return StandardPaths.writableLocation(StandardPaths.AppDataLocation) + "/prefixes/" + game.id
|
||||
}
|
||||
|
||||
function toFileUrl(path) {
|
||||
let p = String(path || "").trim()
|
||||
if (p === "") return ""
|
||||
if (p.startsWith("~/")) {
|
||||
p = StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + p.substring(2)
|
||||
}
|
||||
if (p.startsWith("file:")) return p
|
||||
if (p.startsWith("/")) {
|
||||
return "file:///" + encodeURI(p.substring(1))
|
||||
}
|
||||
return "file:///" + encodeURI(p)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GamepadManager
|
||||
function onNavigateUp() {
|
||||
|
|
@ -325,26 +497,11 @@ Kirigami.Dialog {
|
|||
title: i18n("Compatibility")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
QQC2.Label {
|
||||
text: i18n("Runner")
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
QQC2.ComboBox {
|
||||
id: runnerCombo
|
||||
Layout.fillWidth: true
|
||||
model: [i18n("Native"), i18n("Wine"), i18n("Proton"), i18n("Custom")]
|
||||
}
|
||||
}
|
||||
FormCard.FormComboBoxDelegate {
|
||||
id: runnerCombo
|
||||
text: i18n("Runner")
|
||||
model: runnerChoicesModel
|
||||
textRole: "label"
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
|
@ -353,8 +510,8 @@ Kirigami.Dialog {
|
|||
id: runnerPathField
|
||||
label: i18n("Runner Path")
|
||||
text: ""
|
||||
placeholderText: runnerCombo.currentIndex === 2 ? i18n("Auto-detect Proton") : (runnerCombo.currentIndex === 1 ? i18n("Use system Wine") : i18n("Required for Custom"))
|
||||
enabled: runnerCombo.currentIndex !== 0
|
||||
placeholderText: dialog.currentRunnerChoiceKind() === "proton" ? i18n("Auto-detect Proton") : (dialog.currentRunnerChoiceKind() === "wine" ? i18n("Use system Wine") : i18n("Required for Custom"))
|
||||
enabled: dialog.currentRunnerChoiceKind() === "custom" || dialog.currentRunnerChoiceKind() === "wine" || dialog.currentRunnerChoiceKind() === "proton"
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
|
@ -362,7 +519,7 @@ Kirigami.Dialog {
|
|||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Browse Runner...")
|
||||
icon.name: "document-open"
|
||||
enabled: runnerCombo.currentIndex !== 0
|
||||
enabled: dialog.currentRunnerChoiceKind() === "custom" || dialog.currentRunnerChoiceKind() === "wine" || dialog.currentRunnerChoiceKind() === "proton"
|
||||
onClicked: runnerFileDialog.open()
|
||||
}
|
||||
|
||||
|
|
@ -373,7 +530,7 @@ Kirigami.Dialog {
|
|||
label: i18n("Prefix Path")
|
||||
text: ""
|
||||
placeholderText: i18n("Default (AppDataLocation/prefixes/<gameId>)")
|
||||
enabled: runnerCombo.currentIndex === 1 || runnerCombo.currentIndex === 2
|
||||
enabled: dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton"
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
|
@ -381,9 +538,52 @@ Kirigami.Dialog {
|
|||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Browse Prefix...")
|
||||
icon.name: "document-open-folder"
|
||||
enabled: runnerCombo.currentIndex === 1 || runnerCombo.currentIndex === 2
|
||||
enabled: dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton"
|
||||
onClicked: prefixFolderDialog.open()
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Open Prefix Folder")
|
||||
icon.name: "folder-open"
|
||||
enabled: isEditing && (dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton")
|
||||
onClicked: {
|
||||
let p = dialog.effectivePrefixPath()
|
||||
if (p === "") return
|
||||
Qt.openUrlExternally(dialog.toFileUrl(p))
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Delete Prefix")
|
||||
icon.name: "edit-delete"
|
||||
enabled: isEditing
|
||||
&& (dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton")
|
||||
&& (prefixPathField.text.trim() === "")
|
||||
onClicked: deletePrefixConfirmDialog.open()
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Create Prefix")
|
||||
icon.name: "folder-new"
|
||||
enabled: isEditing
|
||||
&& (dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton")
|
||||
onClicked: App.runnerManager.ensurePrefix(game.id, dialog.currentRunnerChoiceRunnerType(), prefixPathField.text.trim())
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Reset Prefix to Default")
|
||||
icon.name: "edit-clear"
|
||||
enabled: dialog.currentRunnerChoiceRunnerType() === "wine" || dialog.currentRunnerChoiceRunnerType() === "proton"
|
||||
onClicked: prefixPathField.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormCard {
|
||||
|
|
@ -435,42 +635,37 @@ Kirigami.Dialog {
|
|||
onClicked: envModel.clear()
|
||||
}
|
||||
|
||||
Item {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
Repeater {
|
||||
model: envModel
|
||||
|
||||
Repeater {
|
||||
model: envModel
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
RowLayout {
|
||||
QQC2.TextField {
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
Layout.preferredWidth: 1
|
||||
placeholderText: i18n("KEY")
|
||||
text: key
|
||||
onTextChanged: envModel.setProperty(index, "key", text)
|
||||
}
|
||||
|
||||
QQC2.TextField {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1
|
||||
placeholderText: i18n("KEY")
|
||||
text: key
|
||||
onTextChanged: envModel.setProperty(index, "key", text)
|
||||
}
|
||||
QQC2.TextField {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 2
|
||||
placeholderText: i18n("VALUE")
|
||||
text: value
|
||||
onTextChanged: envModel.setProperty(index, "value", text)
|
||||
}
|
||||
|
||||
QQC2.TextField {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 2
|
||||
placeholderText: i18n("VALUE")
|
||||
text: value
|
||||
onTextChanged: envModel.setProperty(index, "value", text)
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
icon.name: "list-remove"
|
||||
onClicked: envModel.remove(index)
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "list-remove"
|
||||
onClicked: envModel.remove(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -630,9 +825,35 @@ Kirigami.Dialog {
|
|||
}
|
||||
|
||||
onOpened: {
|
||||
App.runnerManager.refreshRunners()
|
||||
loadFields()
|
||||
nameField.forceActiveFocus()
|
||||
}
|
||||
|
||||
onGameChanged: loadFields()
|
||||
|
||||
Kirigami.PromptDialog {
|
||||
id: deletePrefixConfirmDialog
|
||||
title: i18n("Delete Prefix")
|
||||
subtitle: (isEditing && game)
|
||||
? i18n("Delete the prefix for '%1'? This will remove the entire prefix directory.", game.name)
|
||||
: i18n("Delete this prefix? This will remove the entire prefix directory.")
|
||||
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No
|
||||
onAccepted: {
|
||||
if (!isEditing || !game) return
|
||||
App.runnerManager.deletePrefix(game.id, prefixPathField.text.trim())
|
||||
}
|
||||
onOpened: {
|
||||
Qt.callLater(function() {
|
||||
if (typeof deletePrefixConfirmDialog.standardButton === "function") {
|
||||
let noButton = deletePrefixConfirmDialog.standardButton(Kirigami.Dialog.No)
|
||||
if (noButton) {
|
||||
noButton.forceActiveFocus()
|
||||
return
|
||||
}
|
||||
}
|
||||
deletePrefixConfirmDialog.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ Kirigami.ApplicationWindow {
|
|||
|
||||
function currentConfirmDialog() {
|
||||
if (removeGameConfirmDialog.opened) return removeGameConfirmDialog
|
||||
if (gameEditDialog.visible && gameEditDialog.anyConfirmOpen) return gameEditDialog.currentConfirmDialog()
|
||||
let layerContent = root.settingsLayerContentItem()
|
||||
if (settingsLayerOpen && layerContent && layerContent.anyConfirmOpen) return layerContent.currentConfirmDialog()
|
||||
if (settingsSheet.opened && settingsContent.anyConfirmOpen) return settingsContent.currentConfirmDialog()
|
||||
|
|
@ -177,6 +178,10 @@ Kirigami.ApplicationWindow {
|
|||
return true
|
||||
}
|
||||
if (gameEditDialog.visible) {
|
||||
if (gameEditDialog.anyConfirmOpen) {
|
||||
gameEditDialog.closeCurrentConfirmDialog()
|
||||
return true
|
||||
}
|
||||
if (gameEditDialog.anyMenuOpen) {
|
||||
gameEditDialog.closeCurrentMenu()
|
||||
return true
|
||||
|
|
@ -2041,6 +2046,17 @@ Kirigami.ApplicationWindow {
|
|||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: App.runnerManager
|
||||
|
||||
function onLastErrorChanged() {
|
||||
if (!App.runnerManager) return
|
||||
let msg = String(App.runnerManager.lastError || "")
|
||||
if (msg.trim() === "") return
|
||||
showPassiveNotification(i18n("Runner error: %1", msg), "long")
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!root.isMobile) {
|
||||
const savedW = App.config.windowWidth
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ ColumnLayout {
|
|||
id: settingsPage
|
||||
spacing: Kirigami.Units.mediumSpacing
|
||||
|
||||
property string runnerInstallUrl: ""
|
||||
property string runnerInstallSha256: ""
|
||||
property string runnerInstallName: ""
|
||||
|
||||
function focusFirstControl() {
|
||||
showPlatformBadgesDelegate.forceActiveFocus()
|
||||
}
|
||||
|
|
@ -546,6 +550,165 @@ FormCard.FormHeader {
|
|||
}
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
Layout.topMargin: Kirigami.Units.mediumSpacing
|
||||
Layout.fillWidth: true
|
||||
title: i18n("Runners")
|
||||
}
|
||||
|
||||
FormCard.FormCard {
|
||||
Layout.fillWidth: true
|
||||
|
||||
FormCard.FormTextFieldDelegate {
|
||||
label: i18n("Archive URL or local path")
|
||||
text: settingsPage.runnerInstallUrl
|
||||
placeholderText: i18n("https://... or /home/.../file.tar.gz")
|
||||
enabled: !App.runnerManager.busy
|
||||
onTextChanged: settingsPage.runnerInstallUrl = text
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormTextFieldDelegate {
|
||||
label: i18n("SHA256/SHA512 (optional)")
|
||||
text: settingsPage.runnerInstallSha256
|
||||
placeholderText: i18n("Leave empty to skip verification")
|
||||
enabled: !App.runnerManager.busy
|
||||
onTextChanged: settingsPage.runnerInstallSha256 = text
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormTextFieldDelegate {
|
||||
label: i18n("Name (optional)")
|
||||
text: settingsPage.runnerInstallName
|
||||
placeholderText: i18n("Proton-GE")
|
||||
enabled: !App.runnerManager.busy
|
||||
onTextChanged: settingsPage.runnerInstallName = text
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Install from URL")
|
||||
description: App.runnerManager.busy ? App.runnerManager.status : i18n("Download and install runner")
|
||||
icon.name: "download"
|
||||
enabled: settingsPage.runnerInstallUrl.trim() !== "" && !App.runnerManager.busy
|
||||
onClicked: App.runnerManager.installRunnerFromUrl(settingsPage.runnerInstallUrl, settingsPage.runnerInstallSha256, settingsPage.runnerInstallName)
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Cancel install")
|
||||
icon.name: "dialog-cancel"
|
||||
enabled: App.runnerManager.busy
|
||||
onClicked: App.runnerManager.cancelCurrentInstall()
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: App.runnerManager.status
|
||||
wrapMode: Text.WordWrap
|
||||
visible: App.runnerManager.status !== ""
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: App.runnerManager.lastError
|
||||
wrapMode: Text.WordWrap
|
||||
visible: App.runnerManager.lastError !== ""
|
||||
color: Kirigami.Theme.negativeTextColor
|
||||
}
|
||||
|
||||
QQC2.ProgressBar {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: App.runnerManager.totalBytes > 0 ? App.runnerManager.totalBytes : 1
|
||||
value: App.runnerManager.receivedBytes
|
||||
indeterminate: App.runnerManager.busy && App.runnerManager.totalBytes <= 0
|
||||
visible: App.runnerManager.busy
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
Layout.topMargin: Kirigami.Units.mediumSpacing
|
||||
Layout.fillWidth: true
|
||||
title: i18n("Installed Runners")
|
||||
}
|
||||
|
||||
FormCard.FormCard {
|
||||
Layout.fillWidth: true
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Refresh")
|
||||
icon.name: "view-refresh"
|
||||
enabled: !App.runnerManager.busy
|
||||
onClicked: App.runnerManager.refreshRunners()
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
Repeater {
|
||||
model: App.runnerManager.runners
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
readonly property var runner: modelData
|
||||
readonly property string runnerId: (runner && runner.id) ? runner.id : ""
|
||||
readonly property string runnerName: (runner && runner.name) ? runner.name : runnerId
|
||||
readonly property string runnerType: (runner && runner.type) ? runner.type : ""
|
||||
readonly property string runnerSource: (runner && runner.source) ? runner.source : ""
|
||||
readonly property string runnerPath: (runner && runner.path) ? runner.path : ""
|
||||
|
||||
FormCard.FormTextDelegate {
|
||||
text: runnerName
|
||||
description: (runnerType !== "" ? (runnerType + " · ") : "") + (runnerSource !== "" ? runnerSource : "")
|
||||
textItem.wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormTextDelegate {
|
||||
text: runnerPath
|
||||
textItem.wrapMode: Text.WrapAnywhere
|
||||
textItem.font: Kirigami.Theme.smallFont
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18n("Uninstall")
|
||||
icon.name: "edit-delete"
|
||||
enabled: runnerSource === "installed" && runnerId !== "" && !App.runnerManager.busy
|
||||
onClicked: App.runnerManager.uninstallRunner(runnerId)
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormTextDelegate {
|
||||
visible: App.runnerManager.runners.length === 0
|
||||
text: i18n("No runners found")
|
||||
textItem.font: Kirigami.Theme.smallFont
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
Layout.topMargin: Kirigami.Units.mediumSpacing
|
||||
Layout.fillWidth: true
|
||||
|
|
|
|||
41
src/runner/CMakeLists.txt
Normal file
41
src/runner/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||
|
||||
add_executable(alakarte_runnerd
|
||||
main.cpp
|
||||
runnermanagerdaemon.cpp
|
||||
runnermanagerdaemon.h
|
||||
)
|
||||
|
||||
target_link_libraries(alakarte_runnerd PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::DBus
|
||||
Qt6::Network
|
||||
)
|
||||
|
||||
set_target_properties(alakarte_runnerd PROPERTIES
|
||||
OUTPUT_NAME "alakarte-runnerd"
|
||||
)
|
||||
|
||||
install(TARGETS alakarte_runnerd ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
|
||||
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus")
|
||||
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd")
|
||||
|
||||
configure_file(dbus/org.kde.ALaKarte.Runner1.service.in
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.ALaKarte.Runner1.service"
|
||||
@ONLY
|
||||
)
|
||||
|
||||
configure_file(systemd/org.kde.ALaKarte.Runner1.service.in
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.ALaKarte.Runner1.service"
|
||||
@ONLY
|
||||
)
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.ALaKarte.Runner1.service"
|
||||
DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}
|
||||
)
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.ALaKarte.Runner1.service"
|
||||
DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}
|
||||
)
|
||||
4
src/runner/dbus/org.kde.ALaKarte.Runner1.service.in
Normal file
4
src/runner/dbus/org.kde.ALaKarte.Runner1.service.in
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[D-BUS Service]
|
||||
Name=org.kde.ALaKarte.Runner1
|
||||
Exec=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-runnerd
|
||||
SystemdService=org.kde.ALaKarte.Runner1.service
|
||||
20
src/runner/main.cpp
Normal file
20
src/runner/main.cpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDBusConnection>
|
||||
|
||||
#include "runnermanagerdaemon.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
QCoreApplication::setApplicationName(QStringLiteral("alakarte"));
|
||||
|
||||
RunnerManagerDaemon daemon;
|
||||
if (!daemon.init()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
1091
src/runner/runnermanagerdaemon.cpp
Normal file
1091
src/runner/runnermanagerdaemon.cpp
Normal file
File diff suppressed because it is too large
Load diff
84
src/runner/runnermanagerdaemon.h
Normal file
84
src/runner/runnermanagerdaemon.h
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QSet>
|
||||
#include <QVariantMap>
|
||||
|
||||
class QNetworkReply;
|
||||
class QProcess;
|
||||
|
||||
class RunnerManagerDaemon : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_CLASSINFO("D-Bus Interface", "org.kde.ALaKarte.Runner1")
|
||||
|
||||
public:
|
||||
explicit RunnerManagerDaemon(QObject *parent = nullptr);
|
||||
|
||||
bool init();
|
||||
|
||||
public Q_SLOTS:
|
||||
QString Ping() const;
|
||||
void Version(uint &major, uint &minor) const;
|
||||
|
||||
QVariantList ListRunners();
|
||||
bool Rescan();
|
||||
|
||||
bool RegisterRunner(const QVariantMap &runner);
|
||||
bool UnregisterRunner(const QString &runnerId);
|
||||
|
||||
QVariantMap InstallRunnerFromArchive(const QVariantMap &spec);
|
||||
QVariantMap InstallRunnerFromUrl(const QVariantMap &spec);
|
||||
QVariantMap UninstallRunner(const QString &runnerId);
|
||||
|
||||
bool CancelInstall(const QString &installId);
|
||||
|
||||
QVariantMap EnsurePrefix(const QVariantMap &spec);
|
||||
QVariantMap DeletePrefix(const QVariantMap &spec);
|
||||
QVariantMap ResolveLaunch(const QVariantMap &spec) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void InstallStarted(const QString &installId, const QVariantMap &spec);
|
||||
void InstallProgress(const QString &installId, qint64 receivedBytes, qint64 totalBytes);
|
||||
void InstallFinished(const QString &installId, const QVariantMap &result);
|
||||
|
||||
private:
|
||||
struct RunnerInfo {
|
||||
QString id;
|
||||
QString type;
|
||||
QString name;
|
||||
QString path;
|
||||
QString source;
|
||||
QString installDir;
|
||||
|
||||
QVariantMap toVariantMap() const;
|
||||
static RunnerInfo fromVariantMap(const QVariantMap &map);
|
||||
bool isValid() const;
|
||||
};
|
||||
|
||||
QString registryPath() const;
|
||||
void loadRegistry();
|
||||
bool saveRegistry() const;
|
||||
|
||||
QList<RunnerInfo> discoverRunners() const;
|
||||
QString defaultProtonExecutable() const;
|
||||
|
||||
static QStringList steamCandidateRoots();
|
||||
static QString findSteamClientInstallPathFromProton(const QString &protonExe);
|
||||
|
||||
RunnerInfo runnerById(const QString &runnerId) const;
|
||||
|
||||
QList<RunnerInfo> m_registered;
|
||||
QList<RunnerInfo> m_discovered;
|
||||
|
||||
QSet<QString> m_activeInstalls;
|
||||
QSet<QString> m_cancelRequested;
|
||||
|
||||
QHash<QString, QPointer<QNetworkReply>> m_installReplies;
|
||||
QHash<QString, QPointer<QProcess>> m_installProcesses;
|
||||
};
|
||||
8
src/runner/systemd/org.kde.ALaKarte.Runner1.service.in
Normal file
8
src/runner/systemd/org.kde.ALaKarte.Runner1.service.in
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[Unit]
|
||||
Description=A-La-Karte Runner Manager
|
||||
|
||||
[Service]
|
||||
ExecStart=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-runnerd
|
||||
Type=dbus
|
||||
BusName=org.kde.ALaKarte.Runner1
|
||||
Restart=no
|
||||
604
src/runnermanagerclient.cpp
Normal file
604
src/runnermanagerclient.cpp
Normal file
|
|
@ -0,0 +1,604 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||
|
||||
#include "runnermanagerclient.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDBusArgument>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusConnectionInterface>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusMetaType>
|
||||
#include <QDBusPendingCall>
|
||||
#include <QDBusPendingCallWatcher>
|
||||
#include <QDBusReply>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QProcess>
|
||||
#include <QSharedPointer>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <QUuid>
|
||||
#include <functional>
|
||||
|
||||
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
|
||||
static const QString kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1");
|
||||
static const QString kRunnerInterface = QStringLiteral("org.kde.ALaKarte.Runner1");
|
||||
|
||||
static QString stableIdForUrl(const QString &type, const QString &url)
|
||||
{
|
||||
const QByteArray digest = QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Sha1).toHex();
|
||||
return type + QStringLiteral(":url:") + QString::fromLatin1(digest);
|
||||
}
|
||||
|
||||
static QString stableIdKeyForInputUrl(QString urlString)
|
||||
{
|
||||
urlString = urlString.trimmed();
|
||||
if (urlString.startsWith(QLatin1String("~/"))) {
|
||||
urlString = QDir::homePath() + QLatin1Char('/') + urlString.mid(2);
|
||||
}
|
||||
|
||||
QUrl url(urlString);
|
||||
if (url.isValid() && url.scheme().toLower() == QLatin1String("file")) {
|
||||
QFileInfo fi(url.toLocalFile());
|
||||
const QString canonical = fi.canonicalFilePath();
|
||||
const QString abs = fi.absoluteFilePath();
|
||||
const QString path = canonical.isEmpty() ? abs : canonical;
|
||||
return QUrl::fromLocalFile(path).toString(QUrl::FullyEncoded);
|
||||
}
|
||||
|
||||
if (url.scheme().isEmpty()) {
|
||||
QFileInfo fi(urlString);
|
||||
if (fi.isAbsolute() || urlString.startsWith(QLatin1String("./")) || urlString.startsWith(QLatin1String("../"))) {
|
||||
const QString canonical = fi.exists() ? fi.canonicalFilePath() : QString();
|
||||
const QString abs = fi.absoluteFilePath();
|
||||
const QString path = canonical.isEmpty() ? abs : canonical;
|
||||
return QUrl::fromLocalFile(path).toString(QUrl::FullyEncoded);
|
||||
}
|
||||
}
|
||||
|
||||
if (url.isValid() && !url.scheme().isEmpty()) {
|
||||
return url.toString(QUrl::FullyEncoded);
|
||||
}
|
||||
|
||||
return urlString;
|
||||
}
|
||||
|
||||
RunnerManagerClient::RunnerManagerClient(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||
if (bus.isConnected()) {
|
||||
bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("InstallStarted"), this, SLOT(onInstallStarted(QString, QVariantMap)));
|
||||
|
||||
bus.connect(kRunnerService,
|
||||
kRunnerPath,
|
||||
kRunnerInterface,
|
||||
QStringLiteral("InstallProgress"),
|
||||
this,
|
||||
SLOT(onInstallProgress(QString, qlonglong, qlonglong)));
|
||||
|
||||
bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("InstallFinished"), this, SLOT(onInstallFinished(QString, QVariantMap)));
|
||||
}
|
||||
|
||||
if (QCoreApplication::instance()) {
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
|
||||
shutdownSpawnedRunnerDaemon();
|
||||
});
|
||||
}
|
||||
|
||||
refreshRunners();
|
||||
}
|
||||
|
||||
RunnerManagerClient::~RunnerManagerClient()
|
||||
{
|
||||
shutdownSpawnedRunnerDaemon();
|
||||
}
|
||||
|
||||
void RunnerManagerClient::shutdownSpawnedRunnerDaemon()
|
||||
{
|
||||
if (!m_runnerdProcess) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_runnerdProcess->state() == QProcess::NotRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_runnerdProcess->terminate();
|
||||
if (!m_runnerdProcess->waitForFinished(1000)) {
|
||||
m_runnerdProcess->kill();
|
||||
m_runnerdProcess->waitForFinished(1000);
|
||||
}
|
||||
}
|
||||
|
||||
void RunnerManagerClient::ensureRunnerDaemon()
|
||||
{
|
||||
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||
if (!bus.isConnected() || !bus.interface()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bus.interface()->isServiceRegistered(kRunnerService)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool activatable = false;
|
||||
{
|
||||
const QDBusReply<QStringList> names = bus.interface()->activatableServiceNames();
|
||||
if (names.isValid()) {
|
||||
activatable = names.value().contains(kRunnerService);
|
||||
}
|
||||
}
|
||||
|
||||
const QDBusReply<void> reply = bus.interface()->startService(kRunnerService);
|
||||
if (reply.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bus.interface()->isServiceRegistered(kRunnerService)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activatable) {
|
||||
setLastError(reply.error().message());
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(QT_NO_DEBUG)
|
||||
setLastError(QStringLiteral("Runner service is not available (missing DBus activation for org.kde.ALaKarte.Runner1)"));
|
||||
return;
|
||||
#endif
|
||||
|
||||
if (m_runnerdSpawnAttempted) {
|
||||
return;
|
||||
}
|
||||
m_runnerdSpawnAttempted = true;
|
||||
|
||||
QString program = QCoreApplication::applicationDirPath() + QLatin1String("/alakarte-runnerd");
|
||||
if (!QFileInfo::exists(program)) {
|
||||
program = QStringLiteral("alakarte-runnerd");
|
||||
}
|
||||
|
||||
if (!m_runnerdProcess) {
|
||||
m_runnerdProcess = new QProcess(this);
|
||||
m_runnerdProcess->setProcessChannelMode(QProcess::MergedChannels);
|
||||
connect(m_runnerdProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) {
|
||||
setLastError(QStringLiteral("Failed to start runner service"));
|
||||
});
|
||||
connect(m_runnerdProcess, &QProcess::finished, this, [this](int, QProcess::ExitStatus) {
|
||||
m_runnerdProcess->deleteLater();
|
||||
m_runnerdProcess = nullptr;
|
||||
});
|
||||
}
|
||||
|
||||
m_runnerdProcess->setProgram(program);
|
||||
m_runnerdProcess->setArguments({});
|
||||
m_runnerdProcess->start();
|
||||
}
|
||||
|
||||
bool RunnerManagerClient::busy() const
|
||||
{
|
||||
return m_busy;
|
||||
}
|
||||
|
||||
QString RunnerManagerClient::installId() const
|
||||
{
|
||||
return m_installId;
|
||||
}
|
||||
|
||||
qint64 RunnerManagerClient::receivedBytes() const
|
||||
{
|
||||
return m_receivedBytes;
|
||||
}
|
||||
|
||||
qint64 RunnerManagerClient::totalBytes() const
|
||||
{
|
||||
return m_totalBytes;
|
||||
}
|
||||
|
||||
QString RunnerManagerClient::status() const
|
||||
{
|
||||
return m_status;
|
||||
}
|
||||
|
||||
QString RunnerManagerClient::lastError() const
|
||||
{
|
||||
return m_lastError;
|
||||
}
|
||||
|
||||
QVariantList RunnerManagerClient::runners() const
|
||||
{
|
||||
return m_runners;
|
||||
}
|
||||
|
||||
static QString normalizeHex(QString s)
|
||||
{
|
||||
s = s.trimmed().toLower();
|
||||
const int colon = s.indexOf(QLatin1Char(':'));
|
||||
if (colon >= 0) {
|
||||
const QString prefix = s.left(colon);
|
||||
if (prefix == QLatin1String("sha256") || prefix == QLatin1String("sha512")) {
|
||||
s = s.mid(colon + 1);
|
||||
}
|
||||
}
|
||||
|
||||
QString out;
|
||||
out.reserve(s.size());
|
||||
for (const QChar &c : s) {
|
||||
if ((c >= QLatin1Char('0') && c <= QLatin1Char('9')) || (c >= QLatin1Char('a') && c <= QLatin1Char('f'))) {
|
||||
out.push_back(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void RunnerManagerClient::installRunnerFromUrl(const QString &url, const QString &sha256, const QString &name, const QString &type, bool overwrite)
|
||||
{
|
||||
const QString trimmedUrl = url.trimmed();
|
||||
if (trimmedUrl.isEmpty() || m_busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString effectiveType = type.trimmed().isEmpty() ? QStringLiteral("proton") : type.trimmed();
|
||||
|
||||
setLastError(QString());
|
||||
setProgress(0, 0);
|
||||
setStatus(QStringLiteral("Starting..."));
|
||||
|
||||
const QString newInstallId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
setInstallId(newInstallId);
|
||||
setBusy(true);
|
||||
|
||||
QVariantMap spec;
|
||||
spec.insert(QStringLiteral("installId"), newInstallId);
|
||||
spec.insert(QStringLiteral("url"), trimmedUrl);
|
||||
spec.insert(QStringLiteral("type"), effectiveType);
|
||||
spec.insert(QStringLiteral("name"), name.trimmed());
|
||||
spec.insert(QStringLiteral("overwrite"), overwrite);
|
||||
|
||||
const QString expected = normalizeHex(sha256);
|
||||
if (!expected.isEmpty()) {
|
||||
if (expected.size() == 128) {
|
||||
spec.insert(QStringLiteral("sha512"), expected);
|
||||
} else {
|
||||
spec.insert(QStringLiteral("sha256"), expected);
|
||||
}
|
||||
}
|
||||
|
||||
const QString stableId = stableIdForUrl(effectiveType, stableIdKeyForInputUrl(trimmedUrl));
|
||||
spec.insert(QStringLiteral("id"), stableId);
|
||||
|
||||
ensureRunnerDaemon();
|
||||
|
||||
auto attempts = QSharedPointer<int>::create(0);
|
||||
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
||||
*callPtr = [this, spec, attempts, callPtr]() {
|
||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
||||
QDBusPendingCall call = iface.asyncCall(QStringLiteral("InstallRunnerFromUrl"), spec);
|
||||
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, spec, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
||||
QDBusPendingReply<QVariantMap> reply = *w;
|
||||
w->deleteLater();
|
||||
if (reply.isError()) {
|
||||
const QDBusError::ErrorType t = reply.error().type();
|
||||
if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) {
|
||||
(*attempts)++;
|
||||
ensureRunnerDaemon();
|
||||
QTimer::singleShot(500, this, [callPtr]() {
|
||||
(*callPtr)();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(QStringLiteral("Failed to call InstallRunnerFromUrl"));
|
||||
setLastError(reply.error().message());
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
QVariantMap result = reply.value();
|
||||
const bool ok = result.value(QStringLiteral("ok")).toBool();
|
||||
if (!ok) {
|
||||
const QString err = result.value(QStringLiteral("error")).toString();
|
||||
setLastError(err);
|
||||
setStatus(err.isEmpty() ? QStringLiteral("Failed") : err);
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(QStringLiteral("Started"));
|
||||
});
|
||||
};
|
||||
(*callPtr)();
|
||||
}
|
||||
|
||||
void RunnerManagerClient::deletePrefix(const QString &gameId, const QString &prefixPath)
|
||||
{
|
||||
const QString id = gameId.trimmed();
|
||||
const QString p = prefixPath.trimmed();
|
||||
|
||||
if (id.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureRunnerDaemon();
|
||||
|
||||
QVariantMap spec;
|
||||
spec.insert(QStringLiteral("gameId"), id);
|
||||
if (!p.isEmpty()) {
|
||||
spec.insert(QStringLiteral("prefixPath"), p);
|
||||
}
|
||||
|
||||
auto attempts = QSharedPointer<int>::create(0);
|
||||
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
||||
*callPtr = [this, spec, id, attempts, callPtr]() {
|
||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
||||
QDBusPendingCall call = iface.asyncCall(QStringLiteral("DeletePrefix"), spec);
|
||||
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
||||
QDBusPendingReply<QVariantMap> reply = *w;
|
||||
w->deleteLater();
|
||||
if (reply.isError()) {
|
||||
const QDBusError::ErrorType t = reply.error().type();
|
||||
if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) {
|
||||
(*attempts)++;
|
||||
ensureRunnerDaemon();
|
||||
QTimer::singleShot(500, this, [callPtr]() {
|
||||
(*callPtr)();
|
||||
});
|
||||
return;
|
||||
}
|
||||
setLastError(reply.error().message());
|
||||
return;
|
||||
}
|
||||
|
||||
const QVariantMap result = reply.value();
|
||||
if (!result.value(QStringLiteral("ok")).toBool()) {
|
||||
setLastError(result.value(QStringLiteral("error")).toString());
|
||||
return;
|
||||
}
|
||||
|
||||
const QString removed = result.value(QStringLiteral("prefixPath")).toString();
|
||||
Q_EMIT prefixDeleted(id, removed);
|
||||
});
|
||||
};
|
||||
(*callPtr)();
|
||||
}
|
||||
|
||||
void RunnerManagerClient::cancelCurrentInstall()
|
||||
{
|
||||
if (!m_busy || m_installId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(QStringLiteral("Cancelling..."));
|
||||
|
||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
||||
iface.asyncCall(QStringLiteral("CancelInstall"), m_installId);
|
||||
}
|
||||
|
||||
void RunnerManagerClient::onInstallStarted(const QString &installId, const QVariantMap &)
|
||||
{
|
||||
if (installId.isEmpty() || installId != m_installId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(QStringLiteral("Downloading..."));
|
||||
}
|
||||
|
||||
void RunnerManagerClient::onInstallProgress(const QString &installId, qlonglong received, qlonglong total)
|
||||
{
|
||||
if (installId.isEmpty() || installId != m_installId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setProgress(static_cast<qint64>(received), static_cast<qint64>(total));
|
||||
}
|
||||
|
||||
void RunnerManagerClient::onInstallFinished(const QString &installId, const QVariantMap &result)
|
||||
{
|
||||
if (installId.isEmpty() || installId != m_installId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool ok = result.value(QStringLiteral("ok")).toBool();
|
||||
if (!ok) {
|
||||
const QString err = result.value(QStringLiteral("error")).toString();
|
||||
setLastError(err);
|
||||
setStatus(err.isEmpty() ? QStringLiteral("Failed") : err);
|
||||
} else {
|
||||
setStatus(QStringLiteral("Installed"));
|
||||
}
|
||||
|
||||
setBusy(false);
|
||||
|
||||
refreshRunners();
|
||||
}
|
||||
|
||||
void RunnerManagerClient::refreshRunners()
|
||||
{
|
||||
ensureRunnerDaemon();
|
||||
|
||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
||||
QDBusPendingCall call = iface.asyncCall(QStringLiteral("ListRunners"));
|
||||
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
|
||||
QDBusPendingReply<QVariantList> reply = *w;
|
||||
w->deleteLater();
|
||||
if (reply.isError()) {
|
||||
if (!m_refreshRetryPending && (reply.error().type() == QDBusError::ServiceUnknown || reply.error().type() == QDBusError::NoReply)) {
|
||||
m_refreshRetryPending = true;
|
||||
ensureRunnerDaemon();
|
||||
QTimer::singleShot(500, this, [this]() {
|
||||
m_refreshRetryPending = false;
|
||||
refreshRunners();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const QVariantList raw = reply.value();
|
||||
|
||||
QVariantList list;
|
||||
list.reserve(raw.size());
|
||||
for (const QVariant &v : raw) {
|
||||
if (v.metaType() == QMetaType::fromType<QVariantMap>()) {
|
||||
list.push_back(v.toMap());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (v.metaType() == QMetaType::fromType<QDBusArgument>()) {
|
||||
const QDBusArgument arg = v.value<QDBusArgument>();
|
||||
const QVariantMap map = qdbus_cast<QVariantMap>(arg);
|
||||
list.push_back(map);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (v.canConvert<QVariantMap>()) {
|
||||
list.push_back(v.toMap());
|
||||
continue;
|
||||
}
|
||||
|
||||
list.push_back(QVariantMap{});
|
||||
}
|
||||
|
||||
if (m_runners == list) {
|
||||
return;
|
||||
}
|
||||
m_runners = list;
|
||||
Q_EMIT runnersChanged();
|
||||
});
|
||||
}
|
||||
|
||||
void RunnerManagerClient::uninstallRunner(const QString &runnerId)
|
||||
{
|
||||
const QString id = runnerId.trimmed();
|
||||
if (id.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
||||
QDBusPendingCall call = iface.asyncCall(QStringLiteral("UninstallRunner"), id);
|
||||
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
|
||||
QDBusPendingReply<QVariantMap> reply = *w;
|
||||
w->deleteLater();
|
||||
if (reply.isError()) {
|
||||
setLastError(reply.error().message());
|
||||
return;
|
||||
}
|
||||
|
||||
const QVariantMap result = reply.value();
|
||||
if (!result.value(QStringLiteral("ok")).toBool()) {
|
||||
setLastError(result.value(QStringLiteral("error")).toString());
|
||||
}
|
||||
refreshRunners();
|
||||
});
|
||||
}
|
||||
|
||||
void RunnerManagerClient::ensurePrefix(const QString &gameId, const QString &runner, const QString &prefixPath)
|
||||
{
|
||||
const QString id = gameId.trimmed();
|
||||
const QString r = runner.trimmed();
|
||||
const QString p = prefixPath.trimmed();
|
||||
|
||||
if (id.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureRunnerDaemon();
|
||||
|
||||
QVariantMap spec;
|
||||
spec.insert(QStringLiteral("gameId"), id);
|
||||
if (!r.isEmpty()) {
|
||||
spec.insert(QStringLiteral("runner"), r);
|
||||
}
|
||||
if (!p.isEmpty()) {
|
||||
spec.insert(QStringLiteral("prefixPath"), p);
|
||||
}
|
||||
|
||||
auto attempts = QSharedPointer<int>::create(0);
|
||||
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
||||
*callPtr = [this, spec, id, attempts, callPtr]() {
|
||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
||||
QDBusPendingCall call = iface.asyncCall(QStringLiteral("EnsurePrefix"), spec);
|
||||
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
||||
QDBusPendingReply<QVariantMap> reply = *w;
|
||||
w->deleteLater();
|
||||
if (reply.isError()) {
|
||||
const QDBusError::ErrorType t = reply.error().type();
|
||||
if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) {
|
||||
(*attempts)++;
|
||||
ensureRunnerDaemon();
|
||||
QTimer::singleShot(500, this, [callPtr]() {
|
||||
(*callPtr)();
|
||||
});
|
||||
return;
|
||||
}
|
||||
setLastError(reply.error().message());
|
||||
return;
|
||||
}
|
||||
|
||||
const QVariantMap result = reply.value();
|
||||
if (!result.value(QStringLiteral("ok")).toBool()) {
|
||||
setLastError(result.value(QStringLiteral("error")).toString());
|
||||
return;
|
||||
}
|
||||
|
||||
const QString ensured = result.value(QStringLiteral("prefixPath")).toString();
|
||||
if (!ensured.isEmpty()) {
|
||||
Q_EMIT prefixEnsured(id, ensured);
|
||||
}
|
||||
});
|
||||
};
|
||||
(*callPtr)();
|
||||
}
|
||||
|
||||
void RunnerManagerClient::setBusy(bool busy)
|
||||
{
|
||||
if (m_busy == busy) {
|
||||
return;
|
||||
}
|
||||
m_busy = busy;
|
||||
Q_EMIT busyChanged();
|
||||
}
|
||||
|
||||
void RunnerManagerClient::setInstallId(const QString &installId)
|
||||
{
|
||||
if (m_installId == installId) {
|
||||
return;
|
||||
}
|
||||
m_installId = installId;
|
||||
Q_EMIT installIdChanged();
|
||||
}
|
||||
|
||||
void RunnerManagerClient::setProgress(qint64 received, qint64 total)
|
||||
{
|
||||
if (m_receivedBytes == received && m_totalBytes == total) {
|
||||
return;
|
||||
}
|
||||
m_receivedBytes = received;
|
||||
m_totalBytes = total;
|
||||
Q_EMIT progressChanged();
|
||||
}
|
||||
|
||||
void RunnerManagerClient::setStatus(const QString &status)
|
||||
{
|
||||
if (m_status == status) {
|
||||
return;
|
||||
}
|
||||
m_status = status;
|
||||
Q_EMIT statusChanged();
|
||||
}
|
||||
|
||||
void RunnerManagerClient::setLastError(const QString &error)
|
||||
{
|
||||
if (m_lastError == error) {
|
||||
return;
|
||||
}
|
||||
m_lastError = error;
|
||||
Q_EMIT lastErrorChanged();
|
||||
}
|
||||
85
src/runnermanagerclient.h
Normal file
85
src/runnermanagerclient.h
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariantList>
|
||||
#include <QVariantMap>
|
||||
|
||||
class RunnerManagerClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
|
||||
Q_PROPERTY(QString installId READ installId NOTIFY installIdChanged)
|
||||
Q_PROPERTY(qint64 receivedBytes READ receivedBytes NOTIFY progressChanged)
|
||||
Q_PROPERTY(qint64 totalBytes READ totalBytes NOTIFY progressChanged)
|
||||
Q_PROPERTY(QString status READ status NOTIFY statusChanged)
|
||||
Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
|
||||
Q_PROPERTY(QVariantList runners READ runners NOTIFY runnersChanged)
|
||||
|
||||
public:
|
||||
explicit RunnerManagerClient(QObject *parent = nullptr);
|
||||
~RunnerManagerClient() override;
|
||||
|
||||
bool busy() const;
|
||||
QString installId() const;
|
||||
qint64 receivedBytes() const;
|
||||
qint64 totalBytes() const;
|
||||
QString status() const;
|
||||
QString lastError() const;
|
||||
QVariantList runners() const;
|
||||
|
||||
Q_INVOKABLE void installRunnerFromUrl(const QString &url,
|
||||
const QString &sha256 = QString(),
|
||||
const QString &name = QString(),
|
||||
const QString &type = QStringLiteral("proton"),
|
||||
bool overwrite = true);
|
||||
|
||||
Q_INVOKABLE void cancelCurrentInstall();
|
||||
|
||||
Q_INVOKABLE void refreshRunners();
|
||||
Q_INVOKABLE void uninstallRunner(const QString &runnerId);
|
||||
|
||||
Q_INVOKABLE void ensurePrefix(const QString &gameId, const QString &runner, const QString &prefixPath = QString());
|
||||
|
||||
Q_INVOKABLE void deletePrefix(const QString &gameId, const QString &prefixPath = QString());
|
||||
|
||||
Q_SIGNALS:
|
||||
void busyChanged();
|
||||
void installIdChanged();
|
||||
void progressChanged();
|
||||
void statusChanged();
|
||||
void lastErrorChanged();
|
||||
void runnersChanged();
|
||||
void prefixEnsured(const QString &gameId, const QString &prefixPath);
|
||||
void prefixDeleted(const QString &gameId, const QString &prefixPath);
|
||||
|
||||
private Q_SLOTS:
|
||||
void onInstallStarted(const QString &installId, const QVariantMap &spec);
|
||||
void onInstallProgress(const QString &installId, qlonglong received, qlonglong total);
|
||||
void onInstallFinished(const QString &installId, const QVariantMap &result);
|
||||
|
||||
private:
|
||||
void ensureRunnerDaemon();
|
||||
void shutdownSpawnedRunnerDaemon();
|
||||
void setBusy(bool busy);
|
||||
void setInstallId(const QString &installId);
|
||||
void setProgress(qint64 received, qint64 total);
|
||||
void setStatus(const QString &status);
|
||||
void setLastError(const QString &error);
|
||||
|
||||
bool m_busy = false;
|
||||
QString m_installId;
|
||||
qint64 m_receivedBytes = 0;
|
||||
qint64 m_totalBytes = 0;
|
||||
QString m_status;
|
||||
QString m_lastError;
|
||||
|
||||
QVariantList m_runners;
|
||||
|
||||
bool m_refreshRetryPending = false;
|
||||
|
||||
class QProcess *m_runnerdProcess = nullptr;
|
||||
bool m_runnerdSpawnAttempted = false;
|
||||
};
|
||||
Loading…
Reference in a new issue