diff --git a/app/qml/DevicePage.qml b/app/qml/DevicePage.qml --- a/app/qml/DevicePage.qml +++ b/app/qml/DevicePage.qml @@ -114,6 +114,12 @@ shareIface.shareUrl(fileDialog.fileUrl) } } + PluginItem { + label: i18n("Volume control") + interfaceFactory: RemoteSystemVolumeDbusInterfaceFactory + component: "qrc:/qml/volume.qml" + pluginName: "remotesystemvolume" + } Item { Layout.fillHeight: true } } diff --git a/app/qml/volume.qml b/app/qml/volume.qml new file mode 100644 --- /dev/null +++ b/app/qml/volume.qml @@ -0,0 +1,68 @@ +/* + * Copyright 2018 Nicolas Fella + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.2 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 2.0 as Kirigami +import org.kde.kdeconnect 1.0 + +Kirigami.Page +{ + id: root + title: i18n("Volume control") + property QtObject pluginInterface + + ListView { + id: sinkList + anchors.fill: parent + spacing: Kirigami.Units.largeSpacing + + model: RemoteSinksModel { + deviceId: pluginInterface.deviceId + } + delegate: ColumnLayout { + + width: parent.width + + Label { + text: description + width: parent.width + } + + RowLayout { + + Button { + icon.name: muted ? "player-volume-muted" : "player-volume" + onClicked: pluginInterface.sendMuted(name, !muted) + } + + Slider { + Layout.fillWidth: true + from: 0 + value: volume + to: maxVolume + onMoved: pluginInterface.sendVolume(name, value) + } + } + } + } +} + diff --git a/app/resources.qrc b/app/resources.qrc --- a/app/resources.qrc +++ b/app/resources.qrc @@ -9,5 +9,6 @@ qml/DevicePage.qml qml/FindDevicesPage.qml qml/runcommand.qml + qml/volume.qml diff --git a/declarativeplugin/kdeconnectdeclarativeplugin.cpp b/declarativeplugin/kdeconnectdeclarativeplugin.cpp --- a/declarativeplugin/kdeconnectdeclarativeplugin.cpp +++ b/declarativeplugin/kdeconnectdeclarativeplugin.cpp @@ -33,6 +33,7 @@ #include "interfaces/devicesmodel.h" #include "interfaces/notificationsmodel.h" #include +#include QObject* createDeviceDbusInterface(const QVariant& deviceId) { @@ -95,20 +96,28 @@ return new ShareDbusInterface(deviceId.toString()); } +QObject* createRemoteSystemVolumeInterface(const QVariant& deviceId) +{ + return new RemoteSystemVolumeDbusInterface(deviceId.toString()); +} + + void KdeConnectDeclarativePlugin::registerTypes(const char* uri) { qmlRegisterType(uri, 1, 0, "DevicesModel"); qmlRegisterType(uri, 1, 0, "NotificationsModel"); qmlRegisterType(uri, 1, 0, "RemoteCommandsModel"); qmlRegisterType(uri, 1, 0, "DBusAsyncResponse"); qmlRegisterType(uri, 1, 0, "DevicesSortProxyModel"); + qmlRegisterType(uri, 1, 0, "RemoteSinksModel"); qmlRegisterUncreatableType(uri, 1, 0, "MprisDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); qmlRegisterUncreatableType(uri, 1, 0, "LockDeviceDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); qmlRegisterUncreatableType(uri, 1, 0, "FindMyPhoneDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); qmlRegisterUncreatableType(uri, 1, 0, "RemoteKeyboardDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); qmlRegisterUncreatableType(uri, 1, 0, "DeviceDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); - qmlRegisterUncreatableType(uri, 1, 0, "RemoteCommandsDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); - qmlRegisterUncreatableType(uri, 1, 0, "ShareDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); + qmlRegisterUncreatableType(uri, 1, 0, "RemoteCommandsDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); + qmlRegisterUncreatableType(uri, 1, 0, "RemoteSystemVolumeInterface", QStringLiteral("You're not supposed to instantiate interfaces")); + qmlRegisterUncreatableType(uri, 1, 0, "ShareDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); qmlRegisterSingletonType(uri, 1, 0, "DaemonDbusInterface", [](QQmlEngine*, QJSEngine*) -> QObject* { return new DaemonDbusInterface; @@ -158,4 +167,8 @@ engine->rootContext()->setContextProperty(QStringLiteral("ShareDbusInterfaceFactory") , new ObjectFactory(engine, createShareInterface)); + + engine->rootContext()->setContextProperty(QStringLiteral("RemoteSystemVolumeDbusInterfaceFactory") + , new ObjectFactory(engine, createRemoteSystemVolumeInterface)); + } diff --git a/interfaces/CMakeLists.txt b/interfaces/CMakeLists.txt --- a/interfaces/CMakeLists.txt +++ b/interfaces/CMakeLists.txt @@ -19,6 +19,7 @@ devicessortproxymodel.cpp conversationmessage.cpp remotecommandsmodel.cpp + remotesinksmodel.cpp # modeltest.cpp ) @@ -51,6 +52,8 @@ geninterface(${CMAKE_SOURCE_DIR}/plugins/sms/smsplugin.h smsinterface) geninterface(${CMAKE_SOURCE_DIR}/plugins/sms/conversationsdbusinterface.h conversationsinterface) geninterface(${CMAKE_SOURCE_DIR}/plugins/share/shareplugin.h shareinterface) +geninterface(${CMAKE_SOURCE_DIR}/plugins/remotesystemvolume/remotesystemvolumeplugin.h remotesystemvolumeinterface) + add_library(kdeconnectinterfaces SHARED ${libkdeconnect_SRC}) diff --git a/interfaces/dbusinterfaces.h b/interfaces/dbusinterfaces.h --- a/interfaces/dbusinterfaces.h +++ b/interfaces/dbusinterfaces.h @@ -38,6 +38,7 @@ #include "interfaces/smsinterface.h" #include "interfaces/conversationsinterface.h" #include "interfaces/shareinterface.h" +#include "interfaces/remotesystemvolumeinterface.h" /** * Using these "proxy" classes just in case we need to rename the @@ -229,6 +230,15 @@ ~ShareDbusInterface() override; }; +class KDECONNECTINTERFACES_EXPORT RemoteSystemVolumeDbusInterface + : public OrgKdeKdeconnectDeviceRemotesystemvolumeInterface +{ + Q_OBJECT +public: + explicit RemoteSystemVolumeDbusInterface(const QString& deviceId, QObject* parent = nullptr); + ~RemoteSystemVolumeDbusInterface() = default; +}; + template static void setWhenAvailable(const QDBusPendingReply& pending, W func, QObject* parent) { diff --git a/interfaces/dbusinterfaces.cpp b/interfaces/dbusinterfaces.cpp --- a/interfaces/dbusinterfaces.cpp +++ b/interfaces/dbusinterfaces.cpp @@ -189,3 +189,8 @@ } ShareDbusInterface::~ShareDbusInterface() = default; + +RemoteSystemVolumeDbusInterface::RemoteSystemVolumeDbusInterface(const QString& deviceId, QObject* parent): + OrgKdeKdeconnectDeviceRemotesystemvolumeInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/" + deviceId + "/remotesystemvolume", QDBusConnection::sessionBus(), parent) +{ +} diff --git a/interfaces/remotesinksmodel.h b/interfaces/remotesinksmodel.h new file mode 100644 --- /dev/null +++ b/interfaces/remotesinksmodel.h @@ -0,0 +1,75 @@ +/** + * Copyright 2018 Nicolas Fella + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef REMOTESINKSMODEL_H +#define REMOTESINKSMODEL_H + +#include + +#include "interfaces/dbusinterfaces.h" + +struct Sink { + QString name; + QString description; + int maxVolume; + int volume; + bool muted; +}; + +class KDECONNECTINTERFACES_EXPORT RemoteSinksModel + : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId NOTIFY deviceIdChanged) + +public: + enum ModelRoles { + NameRole, + DescriptionRole, + MaxVolumeRole, + VolumeRole, + MutedRole + }; + + explicit RemoteSinksModel(QObject* parent = nullptr); + ~RemoteSinksModel() override; + + QString deviceId() const; + void setDeviceId(const QString& deviceId); + + QVariant data(const QModelIndex& index, int role) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + + QHash roleNames() const override; + +private Q_SLOTS: + void refreshSinkList(); + +Q_SIGNALS: + void deviceIdChanged(const QString& value); + void rowsChanged(); + +private: + RemoteSystemVolumeDbusInterface* m_dbusInterface; + QVector m_sinkList; + QString m_deviceId; +}; + +#endif // DEVICESMODEL_H diff --git a/interfaces/remotesinksmodel.cpp b/interfaces/remotesinksmodel.cpp new file mode 100644 --- /dev/null +++ b/interfaces/remotesinksmodel.cpp @@ -0,0 +1,173 @@ +/** + * Copyright 2018 Nicolas Fella + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "remotesinksmodel.h" +#include "interfaces_debug.h" + +#include +#include + +RemoteSinksModel::RemoteSinksModel(QObject* parent) + : QAbstractListModel(parent) + , m_dbusInterface(nullptr) +{ + + connect(this, &QAbstractItemModel::rowsInserted, + this, &RemoteSinksModel::rowsChanged); + connect(this, &QAbstractItemModel::rowsRemoved, + this, &RemoteSinksModel::rowsChanged); + + QDBusServiceWatcher* watcher = new QDBusServiceWatcher(DaemonDbusInterface::activatedService(), + QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this); + connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, &RemoteSinksModel::refreshSinkList); + connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, &RemoteSinksModel::refreshSinkList); +} + +QHash RemoteSinksModel::roleNames() const +{ + //Role names for QML + QHash names = QAbstractItemModel::roleNames(); + names.insert(NameRole, "name"); + names.insert(DescriptionRole, "description"); + names.insert(MaxVolumeRole, "maxVolume"); + names.insert(VolumeRole, "volume"); + names.insert(MutedRole, "muted"); + + return names; +} + +RemoteSinksModel::~RemoteSinksModel() +{ +} + +QString RemoteSinksModel::deviceId() const +{ + return m_deviceId; +} + +void RemoteSinksModel::setDeviceId(const QString& deviceId) +{ + m_deviceId = deviceId; + + if (m_dbusInterface) { + delete m_dbusInterface; + } + + m_dbusInterface = new RemoteSystemVolumeDbusInterface(deviceId, this); + + connect(m_dbusInterface, &OrgKdeKdeconnectDeviceRemotesystemvolumeInterface::sinksChanged, + this, &RemoteSinksModel::refreshSinkList); + + connect(m_dbusInterface, &OrgKdeKdeconnectDeviceRemotesystemvolumeInterface::volumeChanged, this, [this](const QString& name, int volume) { + for (Sink* s: m_sinkList) { + if (s->name == name) { + s->volume = volume; + Q_EMIT dataChanged(index(0,0), index(m_sinkList.size() - 1, 0)); + } + } + }); + + connect(m_dbusInterface, &OrgKdeKdeconnectDeviceRemotesystemvolumeInterface::mutedChanged, this, [this](const QString& name, bool muted) { + for (Sink* s: m_sinkList) { + if (s->name == name) { + s->muted = muted; + Q_EMIT dataChanged(index(0,0), index(m_sinkList.size() - 1, 0)); + } + } + }); + + refreshSinkList(); + + Q_EMIT deviceIdChanged(deviceId); +} + +void RemoteSinksModel::refreshSinkList() +{ + if (!m_dbusInterface) { + return; + } + + if (!m_dbusInterface->isValid()) { + qCWarning(KDECONNECT_INTERFACES) << "dbus interface not valid"; + return; + } + + const auto cmds = QJsonDocument::fromJson(m_dbusInterface->sinks()).array(); + + beginResetModel(); + + qDeleteAll(m_sinkList); + m_sinkList.clear(); + + for (auto it = cmds.constBegin(), itEnd = cmds.constEnd(); it!=itEnd; ++it) { + const QJsonObject cont = it->toObject(); + Sink* sink = new Sink(); + sink->name = cont.value(QStringLiteral("name")).toString(); + sink->description = cont.value(QStringLiteral("description")).toString(); + sink->maxVolume = cont.value(QStringLiteral("maxVolume")).toInt(); + sink->volume = cont.value(QStringLiteral("volume")).toInt(); + sink->muted = cont.value(QStringLiteral("muted")).toBool(); + + m_sinkList.append(sink); + } + + endResetModel(); +} + +QVariant RemoteSinksModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() + || index.row() < 0 + || index.row() >= m_sinkList.count()) + { + return QVariant(); + } + + if (!m_dbusInterface || !m_dbusInterface->isValid()) { + return QVariant(); + } + + Sink* sink = m_sinkList[index.row()]; + + switch (role) { + case NameRole: + return sink->name; + case DescriptionRole: + return sink->description; + case MaxVolumeRole: + return sink->maxVolume; + case VolumeRole: + return sink->volume; + case MutedRole: + return sink->muted; + default: + return QVariant(); + } +} + +int RemoteSinksModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) { + //Return size 0 if we are a child because this is not a tree + return 0; + } + + return m_sinkList.count(); +} diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -37,6 +37,7 @@ add_subdirectory(mprisremote) add_subdirectory(remotecontrol) add_subdirectory(lockdevice) + add_subdirectory(remotesystemvolume) endif() if(KF5PulseAudioQt_FOUND) diff --git a/plugins/remotesystemvolume/CMakeLists.txt b/plugins/remotesystemvolume/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/plugins/remotesystemvolume/CMakeLists.txt @@ -0,0 +1,11 @@ +set(kdeconnect_remotesystemvolume_SRCS + remotesystemvolumeplugin.cpp +) + +kdeconnect_add_plugin(kdeconnect_remotesystemvolume JSON kdeconnect_remotesystemvolume.json SOURCES ${kdeconnect_remotesystemvolume_SRCS}) + +target_link_libraries(kdeconnect_remotesystemvolume + kdeconnectcore + Qt5::DBus + KF5::I18n +) diff --git a/plugins/remotesystemvolume/kdeconnect_remotesystemvolume.json b/plugins/remotesystemvolume/kdeconnect_remotesystemvolume.json new file mode 100644 --- /dev/null +++ b/plugins/remotesystemvolume/kdeconnect_remotesystemvolume.json @@ -0,0 +1,27 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "nicolas.fella@gmx.de", + "Name": "Nicolas Fella" + } + ], + "Description": "Control the volume of the connected device", + "EnabledByDefault": true, + "Icon": "player-volume", + "Id": "kdeconnect_remotesystemvolume", + "License": "GPL", + "Name": "Remote system volume", + "ServiceTypes": [ + "KdeConnect/Plugin" + ], + "Version": "0.1", + "Website": "https://nicolasfella.wordpress.com" + }, + "X-KdeConnect-OutgoingPacketType": [ + "kdeconnect.systemvolume.request" + ], + "X-KdeConnect-SupportedPacketType": [ + "kdeconnect.systemvolume" + ] +} diff --git a/plugins/remotesystemvolume/remotesystemvolumeplugin.h b/plugins/remotesystemvolume/remotesystemvolumeplugin.h new file mode 100644 --- /dev/null +++ b/plugins/remotesystemvolume/remotesystemvolumeplugin.h @@ -0,0 +1,62 @@ +/** + * Copyright 2018 Nicolas Fella + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef REMOTESYSTEMVOLUMEPLUGIN_H +#define REMOTESYSTEMVOLUMEPLUGIN_H + +#include + +#include + +#define PACKET_TYPE_SYSTEMVOLUME QStringLiteral("kdeconnect.systemvolume") +#define PACKET_TYPE_SYSTEMVOLUME_REQUEST QStringLiteral("kdeconnect.systemvolume.request") + +class Q_DECL_EXPORT RemoteSystemVolumePlugin + : public KdeConnectPlugin +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kdeconnect.device.remotesystemvolume") + Q_PROPERTY(QByteArray sinks READ sinks NOTIFY sinksChanged) + Q_PROPERTY(QString deviceId READ deviceId CONSTANT) + +public: + explicit RemoteSystemVolumePlugin(QObject* parent, const QVariantList& args); + ~RemoteSystemVolumePlugin() override; + + bool receivePacket(const NetworkPacket& np) override; + void connected() override; + QString dbusPath() const override; + + QString deviceId() const { return device()->id(); } + QByteArray sinks(); + + Q_SCRIPTABLE void sendVolume(const QString& name, int volume); + Q_SCRIPTABLE void sendMuted(const QString& name, bool muted); + +Q_SIGNALS: + Q_SCRIPTABLE void sinksChanged(); + Q_SCRIPTABLE void volumeChanged(const QString& name, int volume); + Q_SCRIPTABLE void mutedChanged(const QString& name, bool muted); + +private: + QByteArray m_sinks; +}; + +#endif // REMOTESYSTEMVOLUMEPLUGIN_H diff --git a/plugins/remotesystemvolume/remotesystemvolumeplugin.cpp b/plugins/remotesystemvolume/remotesystemvolumeplugin.cpp new file mode 100644 --- /dev/null +++ b/plugins/remotesystemvolume/remotesystemvolumeplugin.cpp @@ -0,0 +1,105 @@ +/** + * Copyright 2018 Nicolas Fella + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "remotesystemvolumeplugin.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON( KdeConnectPluginFactory, "kdeconnect_remotesystemvolume.json", registerPlugin< RemoteSystemVolumePlugin >(); ) + +Q_LOGGING_CATEGORY(KDECONNECT_PLUGIN_PING, "kdeconnect.plugin.remotesystemvolume") + +RemoteSystemVolumePlugin::RemoteSystemVolumePlugin(QObject* parent, const QVariantList& args) + : KdeConnectPlugin(parent, args) +{ +} + +RemoteSystemVolumePlugin::~RemoteSystemVolumePlugin() +{ +} + +bool RemoteSystemVolumePlugin::receivePacket(const NetworkPacket& np) +{ + + if (np.has(QStringLiteral("sinkList"))) { + QJsonDocument document(np.get(QStringLiteral("sinkList"))); + m_sinks = document.toJson(); + Q_EMIT sinksChanged(); + } else { + + QString name = np.get(QStringLiteral("name")); + + if (np.has(QStringLiteral("volume"))) { + Q_EMIT volumeChanged(name, np.get(QStringLiteral("volume"))); + } + + if (np.has(QStringLiteral("muted"))) { + Q_EMIT mutedChanged(name, np.get(QStringLiteral("muted"))); + } + } + + return true; +} + +void RemoteSystemVolumePlugin::sendVolume(const QString& name, int volume) +{ + NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME_REQUEST); + np.set(QStringLiteral("name"), name); + np.set(QStringLiteral("volume"), volume); + sendPacket(np); +} + +void RemoteSystemVolumePlugin::sendMuted(const QString& name, bool muted) +{ + NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME_REQUEST); + np.set(QStringLiteral("name"), name); + np.set(QStringLiteral("muted"), muted); + sendPacket(np); +} + +void RemoteSystemVolumePlugin::connected() +{ + NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME_REQUEST); + np.set(QStringLiteral("requestSinks"), true); + sendPacket(np); +} + +QByteArray RemoteSystemVolumePlugin::sinks() +{ + return m_sinks; +} + +QString RemoteSystemVolumePlugin::dbusPath() const +{ + return "/modules/kdeconnect/devices/" + device()->id() + "/remotesystemvolume"; +} + +#include "remotesystemvolumeplugin.moc" +