mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-11-22 01:05:17 +00:00
Add support for server-side notifications
This commit is contained in:
parent
357ac89330
commit
60bc90c5fa
|
@ -100,7 +100,7 @@ public:
|
||||||
Q_PROPERTY(QString userId READ userId NOTIFY userIdChanged)
|
Q_PROPERTY(QString userId READ userId NOTIFY userIdChanged)
|
||||||
Q_PROPERTY(QJsonObject deviceProfile READ deviceProfileJson NOTIFY deviceProfileChanged)
|
Q_PROPERTY(QJsonObject deviceProfile READ deviceProfileJson NOTIFY deviceProfileChanged)
|
||||||
Q_PROPERTY(QString version READ version)
|
Q_PROPERTY(QString version READ version)
|
||||||
Q_PROPERTY(EventBus *eventbus READ eventbus FINAL)
|
Q_PROPERTY(Jellyfin::EventBus *eventbus READ eventbus FINAL)
|
||||||
Q_PROPERTY(Jellyfin::WebSocket *websocket READ websocket FINAL)
|
Q_PROPERTY(Jellyfin::WebSocket *websocket READ websocket FINAL)
|
||||||
Q_PROPERTY(QVariantList supportedCommands READ supportedCommands WRITE setSupportedCommands NOTIFY supportedCommandsChanged)
|
Q_PROPERTY(QVariantList supportedCommands READ supportedCommands WRITE setSupportedCommands NOTIFY supportedCommandsChanged)
|
||||||
Q_PROPERTY(Jellyfin::ViewModel::Settings *settings READ settings NOTIFY settingsChanged)
|
Q_PROPERTY(Jellyfin::ViewModel::Settings *settings READ settings NOTIFY settingsChanged)
|
||||||
|
|
|
@ -45,9 +45,11 @@ signals:
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The server has requested to display an message to the user
|
* @brief The server has requested to display an message to the user
|
||||||
|
* @param header The header of the message.
|
||||||
* @param message The message to show.
|
* @param message The message to show.
|
||||||
|
* @param timeout Timeout in MS to show the message. -1: no timeout supplied.
|
||||||
*/
|
*/
|
||||||
void displayMessage(const QString &message);
|
void displayMessage(const QString &header, const QString &message, int timeout = -1);
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,19 @@ extern template QDateTime fromJsonValue<QDateTime>(const QJsonValue &source, con
|
||||||
extern template QVariant fromJsonValue<QVariant>(const QJsonValue &source, convertType<QVariant>);
|
extern template QVariant fromJsonValue<QVariant>(const QJsonValue &source, convertType<QVariant>);
|
||||||
extern template QUuid fromJsonValue<QUuid>(const QJsonValue &source, convertType<QUuid>);
|
extern template QUuid fromJsonValue<QUuid>(const QJsonValue &source, convertType<QUuid>);
|
||||||
|
|
||||||
|
extern template QJsonValue toJsonValue<int>(const int &source, convertType<int>);
|
||||||
|
extern template QJsonValue toJsonValue<qint64>(const qint64 &source, convertType<qint64>);
|
||||||
|
extern template QJsonValue toJsonValue<bool>(const bool &source, convertType<bool>);
|
||||||
|
extern template QJsonValue toJsonValue<QString>(const QString &source, convertType<QString>);
|
||||||
|
extern template QJsonValue toJsonValue<QStringList>(const QStringList &source, convertType<QStringList>);
|
||||||
|
extern template QJsonValue toJsonValue<QJsonObject>(const QJsonObject &source, convertType<QJsonObject>);
|
||||||
|
extern template QJsonValue toJsonValue<double>(const double &source, convertType<double>);
|
||||||
|
extern template QJsonValue toJsonValue<float>(const float &source, convertType<float>);
|
||||||
|
extern template QJsonValue toJsonValue<QDateTime>(const QDateTime &source, convertType<QDateTime>);
|
||||||
|
extern template QJsonValue toJsonValue<QVariant>(const QVariant &source, convertType<QVariant>);
|
||||||
|
extern template QJsonValue toJsonValue<QUuid>(const QUuid &source, convertType<QUuid>);
|
||||||
|
|
||||||
|
|
||||||
extern template QString toString(const QUuid &source, convertType<QUuid>);
|
extern template QString toString(const QUuid &source, convertType<QUuid>);
|
||||||
extern template QString toString(const qint32 &source, convertType<qint32>);
|
extern template QString toString(const qint32 &source, convertType<qint32>);
|
||||||
extern template QString toString(const qint64 &source, convertType<qint64>);
|
extern template QString toString(const qint64 &source, convertType<qint64>);
|
||||||
|
|
|
@ -33,9 +33,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
#include "apiclient.h"
|
#include "apiclient.h"
|
||||||
|
|
||||||
|
Q_DECLARE_LOGGING_CATEGORY(jellyfinWebSocket);
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
class ApiClient;
|
class ApiClient;
|
||||||
|
|
||||||
|
|
||||||
namespace DTO {
|
namespace DTO {
|
||||||
class UserItemDataDto;
|
class UserItemDataDto;
|
||||||
using UserData = UserItemDataDto;
|
using UserData = UserItemDataDto;
|
||||||
|
|
|
@ -465,7 +465,6 @@ void ApiClient::onUserDataChanged(const QString &itemId, UserData *userData) {
|
||||||
void ApiClient::setAuthenticated(bool authenticated) {
|
void ApiClient::setAuthenticated(bool authenticated) {
|
||||||
Q_D(ApiClient);
|
Q_D(ApiClient);
|
||||||
d->authenticated = authenticated;
|
d->authenticated = authenticated;
|
||||||
if (authenticated) d->webSocket->open();
|
|
||||||
emit authenticatedChanged(authenticated);
|
emit authenticatedChanged(authenticated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
|
|
||||||
#include "JellyfinQt/apiclient.h"
|
#include "JellyfinQt/apiclient.h"
|
||||||
#include "JellyfinQt/apimodel.h"
|
#include "JellyfinQt/apimodel.h"
|
||||||
|
#include "JellyfinQt/eventbus.h"
|
||||||
#include "JellyfinQt/serverdiscoverymodel.h"
|
#include "JellyfinQt/serverdiscoverymodel.h"
|
||||||
#include "JellyfinQt/websocket.h"
|
#include "JellyfinQt/websocket.h"
|
||||||
#include "JellyfinQt/viewmodel/item.h"
|
#include "JellyfinQt/viewmodel/item.h"
|
||||||
|
|
|
@ -28,7 +28,7 @@ QString uuidToString(const QUuid &source) {
|
||||||
QString str = source.toString();
|
QString str = source.toString();
|
||||||
// Convert {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} (length: 38)
|
// Convert {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} (length: 38)
|
||||||
// to xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (lenght: 32)
|
// to xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (lenght: 32)
|
||||||
return QString(str.mid(1, 8) + str.mid(10, 4) + str.mid(15, 4) + str.mid(20, 4) + str.mid(25 + 12));
|
return QString(str.mid(1, 8) + str.mid(10, 4) + str.mid(15, 4) + str.mid(20, 4) + str.mid(25, 12));
|
||||||
}
|
}
|
||||||
QUuid stringToUuid(const QString &source) {
|
QUuid stringToUuid(const QString &source) {
|
||||||
if (source.size() != 32) throw ParseException("Error while trying to parse JSON value as QUid: invalid length");
|
if (source.size() != 32) throw ParseException("Error while trying to parse JSON value as QUid: invalid length");
|
||||||
|
|
|
@ -23,7 +23,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
#include <JellyfinQt/dto/generalcommandtype.h>
|
#include <JellyfinQt/dto/generalcommandtype.h>
|
||||||
#include <JellyfinQt/dto/useritemdatadto.h>
|
#include <JellyfinQt/dto/useritemdatadto.h>
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(jellyfinWebSocket, "jellyfin.websocket");
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
|
|
||||||
WebSocket::WebSocket(ApiClient *client)
|
WebSocket::WebSocket(ApiClient *client)
|
||||||
: QObject (client), m_apiClient(client){
|
: QObject (client), m_apiClient(client){
|
||||||
connect(&m_webSocket, &QWebSocket::connected, this, &WebSocket::onConnected);
|
connect(&m_webSocket, &QWebSocket::connected, this, &WebSocket::onConnected);
|
||||||
|
@ -31,11 +34,17 @@ WebSocket::WebSocket(ApiClient *client)
|
||||||
connect(&m_webSocket, static_cast<void (QWebSocket::*)(QAbstractSocket::SocketError)>(&QWebSocket::error),
|
connect(&m_webSocket, static_cast<void (QWebSocket::*)(QAbstractSocket::SocketError)>(&QWebSocket::error),
|
||||||
this, [this](QAbstractSocket::SocketError error) {
|
this, [this](QAbstractSocket::SocketError error) {
|
||||||
Q_UNUSED(error)
|
Q_UNUSED(error)
|
||||||
qDebug() << "Connection error: " << m_webSocket.errorString();
|
qCDebug(jellyfinWebSocket) << "Connection error: " << m_webSocket.errorString();
|
||||||
});
|
});
|
||||||
connect(&m_webSocket, &QWebSocket::stateChanged, this, &WebSocket::onWebsocketStateChanged);
|
connect(&m_webSocket, &QWebSocket::stateChanged, this, &WebSocket::onWebsocketStateChanged);
|
||||||
connect(&m_keepAliveTimer, &QTimer::timeout, this, &WebSocket::sendKeepAlive);
|
connect(&m_keepAliveTimer, &QTimer::timeout, this, &WebSocket::sendKeepAlive);
|
||||||
connect(&m_retryTimer, &QTimer::timeout, this, &WebSocket::open);
|
connect(&m_retryTimer, &QTimer::timeout, this, &WebSocket::open);
|
||||||
|
connect(client, &ApiClient::authenticatedChanged, this, [this](bool isAuthenticated) {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
this->m_reconnectAttempt = 0;
|
||||||
|
this->open();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocket::open() {
|
void WebSocket::open() {
|
||||||
|
@ -48,7 +57,7 @@ void WebSocket::open() {
|
||||||
connectionUrl.setQuery(query);
|
connectionUrl.setQuery(query);
|
||||||
m_webSocket.open(connectionUrl);
|
m_webSocket.open(connectionUrl);
|
||||||
m_reconnectAttempt++;
|
m_reconnectAttempt++;
|
||||||
qDebug() << "Opening WebSocket connection to " << m_webSocket.requestUrl() << ", connect attempt " << m_reconnectAttempt;
|
qCDebug(jellyfinWebSocket) << "Opening WebSocket connection to " << m_webSocket.requestUrl() << ", connect attempt " << m_reconnectAttempt;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocket::onConnected() {
|
void WebSocket::onConnected() {
|
||||||
|
@ -66,25 +75,47 @@ void WebSocket::onDisconnected() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocket::textMessageReceived(const QString &message) {
|
void WebSocket::textMessageReceived(const QString &message) {
|
||||||
qDebug() << "WebSocket: message received: " << message;
|
qCDebug(jellyfinWebSocket) << "message received: " << message;
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
|
QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
|
||||||
if (doc.isNull() || !doc.isObject()) {
|
if (doc.isNull() || !doc.isObject()) {
|
||||||
qWarning() << "Malformed message received over WebSocket: parse error or root not an object.";
|
qCWarning(jellyfinWebSocket()) << "Malformed message received over WebSocket: parse error or root not an object.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
QJsonObject messageRoot = doc.object();
|
QJsonObject messageRoot = doc.object();
|
||||||
if (!messageRoot.contains("MessageType")) {
|
if (!messageRoot.contains("MessageType")) {
|
||||||
qWarning() << "Malformed message received over WebSocket: no MessageType set.";
|
qCWarning(jellyfinWebSocket) << "Malformed message received over WebSocket: no MessageType set.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the type so we can use it in our enums.
|
// Convert the type so we can use it in our enums.
|
||||||
QString messageTypeStr = messageRoot["MessageType"].toString();
|
QString messageType = messageRoot["MessageType"].toString();
|
||||||
QJsonValue data = messageRoot["Data"];
|
QJsonValue data = messageRoot["Data"];
|
||||||
if (messageTypeStr == QStringLiteral("ForceKeepAlive")) {
|
if (messageType == QStringLiteral("ForceKeepAlive")) {
|
||||||
setupKeepAlive(data.toInt());
|
setupKeepAlive(data.toInt());
|
||||||
|
} else if (messageType == QStringLiteral("GeneralCommand")) {
|
||||||
|
try {
|
||||||
|
DTO::GeneralCommand command = DTO::GeneralCommand::fromJson(messageRoot["Data"].toObject());
|
||||||
|
|
||||||
|
// TODO: move command handling out of here
|
||||||
|
switch(command.name()) {
|
||||||
|
case DTO::GeneralCommandType::DisplayMessage:
|
||||||
|
{
|
||||||
|
QString header = command.arguments()["Header"].toString("Message from server");
|
||||||
|
QString text = command.arguments()["Text"].toString("<Empty message>");
|
||||||
|
int timeout = command.arguments()["TimeoutMs"].toInt(-1);
|
||||||
|
emit m_apiClient->eventbus()->displayMessage(header, text, timeout);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
qCDebug(jellyfinWebSocket) << "Unhandled command: " << messageRoot["Data"];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(QException &e) {
|
||||||
|
qCWarning(jellyfinWebSocket()) << "Error while deserializing command: " << e.what();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
qDebug() << messageTypeStr;
|
qCDebug(jellyfinWebSocket) << messageType;
|
||||||
}
|
}
|
||||||
bool ok;
|
bool ok;
|
||||||
/*MessageType messageType = static_cast<MessageType>(QMetaEnum::fromType<WebSocket::MessageType>().keyToValue(messageTypeStr.toLatin1(), &ok));
|
/*MessageType messageType = static_cast<MessageType>(QMetaEnum::fromType<WebSocket::MessageType>().keyToValue(messageTypeStr.toLatin1(), &ok));
|
||||||
|
@ -145,6 +176,6 @@ void WebSocket::sendMessage(MessageType type, QJsonValue data) {
|
||||||
root["Data"] = data;
|
root["Data"] = data;
|
||||||
QString message = QJsonDocument(root).toJson(QJsonDocument::Compact);
|
QString message = QJsonDocument(root).toJson(QJsonDocument::Compact);
|
||||||
m_webSocket.sendTextMessage(message);
|
m_webSocket.sendTextMessage(message);
|
||||||
qDebug() << "Sent message: " << message;
|
qCDebug(jellyfinWebSocket) << "Sent message: " << message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,5 +18,7 @@
|
||||||
<file>qml/pages/DetailPage.qml</file>
|
<file>qml/pages/DetailPage.qml</file>
|
||||||
<file>qml/ApiClient.qml</file>
|
<file>qml/ApiClient.qml</file>
|
||||||
<file>qml/qmldir</file>
|
<file>qml/qmldir</file>
|
||||||
|
<file>qml/components/NotificationList.qml</file>
|
||||||
|
<file>qml/components/Notification.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|
28
qtquick/qml/components/Notification.qml
Normal file
28
qtquick/qml/components/Notification.qml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import QtQuick 2.6
|
||||||
|
import QtQuick.Controls 2.6
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property alias title: titleText.text
|
||||||
|
property alias content: contentText.text
|
||||||
|
color: "black"
|
||||||
|
implicitHeight: column.height
|
||||||
|
implicitWidth: column.width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: column
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: titleText
|
||||||
|
width: parent.width
|
||||||
|
font.pointSize: 24
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: contentText
|
||||||
|
width: parent.width
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
qtquick/qml/components/NotificationList.qml
Normal file
49
qtquick/qml/components/NotificationList.qml
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import QtQuick 2.6
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: notifList
|
||||||
|
ListModel {
|
||||||
|
id: notificationModel
|
||||||
|
ListElement {
|
||||||
|
title: "Test notification"
|
||||||
|
text: "Foo bar\n2000"
|
||||||
|
timeout: 10000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: notificationModel
|
||||||
|
Notification {
|
||||||
|
id: notify
|
||||||
|
title: model.title
|
||||||
|
content: model.text
|
||||||
|
width: notifList.width
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: timer
|
||||||
|
interval: model.timeout
|
||||||
|
onTriggered: notificationModel.remove(index, 1)
|
||||||
|
}
|
||||||
|
Component.onCompleted: timer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add: Transition {
|
||||||
|
NumberAnimation { targets: ViewTransition.targetItems; properties: "opacity"; from: 0.0; to: 1.0; }
|
||||||
|
}
|
||||||
|
move: Transition {
|
||||||
|
NumberAnimation {
|
||||||
|
targets: ViewTransition.targetItems;
|
||||||
|
properties: "x";
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
targets: ViewTransition.targetItems;
|
||||||
|
properties: "y";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNotification(title, text, timeout) {
|
||||||
|
var realTimeout = Number(timeout) >= 0 ? Number(timeout) : 5000
|
||||||
|
notificationModel.append({"title": title, "text": text, "timeout": realTimeout});
|
||||||
|
}
|
||||||
|
}
|
|
@ -53,6 +53,24 @@ ApplicationWindow {
|
||||||
Keys.onEscapePressed: pop()
|
Keys.onEscapePressed: pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotificationList {
|
||||||
|
id: notifList
|
||||||
|
anchors {
|
||||||
|
right: parent.right
|
||||||
|
bottom: parent.bottom
|
||||||
|
}
|
||||||
|
width: Math.min(parent.width, 400)
|
||||||
|
height: parent.height
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: ApiClient.eventbus
|
||||||
|
onDisplayMessage: {
|
||||||
|
console.log("Displaying message: ", header, ": ", message)
|
||||||
|
notifList.addNotification(header, message, timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: ApiClient
|
target: ApiClient
|
||||||
onSetupRequired: { pageStack.replace(Qt.resolvedUrl("pages/setup/ServerSelectPage.qml")); }
|
onSetupRequired: { pageStack.replace(Qt.resolvedUrl("pages/setup/ServerSelectPage.qml")); }
|
||||||
|
|
|
@ -18,6 +18,8 @@ int main(int argc, char** argv) {
|
||||||
app.setOrganizationDomain("nl.netsoj.chris");
|
app.setOrganizationDomain("nl.netsoj.chris");
|
||||||
app.setOrganizationName("Chris Josten");
|
app.setOrganizationName("Chris Josten");
|
||||||
|
|
||||||
|
qSetMessagePattern("[%{time yyyyMMdd h:mm:ss.zzz} %{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}] %{if-category}<%{category}> %{endif} %{message}");
|
||||||
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||||
// Disable Qt nagging about "implicitly defined onFoo properties in connections are deprecated",
|
// Disable Qt nagging about "implicitly defined onFoo properties in connections are deprecated",
|
||||||
// as we cannot yet move towards a newer version.
|
// as we cannot yet move towards a newer version.
|
||||||
|
|
|
@ -55,7 +55,7 @@ ApplicationWindow {
|
||||||
|
|
||||||
PlatformMediaControl {
|
PlatformMediaControl {
|
||||||
playbackManager: appWindow.playbackManager
|
playbackManager: appWindow.playbackManager
|
||||||
canQuit: fasle
|
canQuit: false
|
||||||
desktopFile: "harbour-sailfin"
|
desktopFile: "harbour-sailfin"
|
||||||
playerName: "Sailfin"
|
playerName: "Sailfin"
|
||||||
canRaise: true
|
canRaise: true
|
||||||
|
@ -100,6 +100,23 @@ ApplicationWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Notification {
|
||||||
|
id: serverNotification
|
||||||
|
//: The application name for the notification
|
||||||
|
appName: qsTr("Sailfin")
|
||||||
|
appIcon: "harbour-sailfin"
|
||||||
|
isTransient: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: apiClient.eventbus
|
||||||
|
onDisplayMessage: {
|
||||||
|
serverNotification.summary = header
|
||||||
|
serverNotification.body = message
|
||||||
|
serverNotification.publish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PlaybackManager {
|
PlaybackManager {
|
||||||
id: _playbackManager
|
id: _playbackManager
|
||||||
apiClient: appWindow.apiClient
|
apiClient: appWindow.apiClient
|
||||||
|
|
Loading…
Reference in a new issue