From d81fa5071598af8c48d073f465484bc5937ca5a5 Mon Sep 17 00:00:00 2001 From: Chris Josten Date: Fri, 9 Oct 2020 02:33:08 +0200 Subject: [PATCH] Models get updated when userData changes at server The websocket now notifies the ApiClient, on which several models and items are listening, when the userData for an user has changed. The UI on the qml side may automatically updates without any extra effort. This also resolves a bug where videos didn't resume after +/- 3:40 due to an integer overflow. --- core/include/credentialmanager.h | 2 +- core/include/jellyfinapiclient.h | 11 +++ core/include/jellyfinapimodel.h | 39 ++++++--- core/include/jellyfinitem.h | 83 ++++++++++++++++-- core/include/jellyfinwebsocket.h | 11 ++- core/src/jellyfinapiclient.cpp | 4 + core/src/jellyfinapimodel.cpp | 68 ++++++++++++++- core/src/jellyfinitem.cpp | 85 ++++++++++++++++--- core/src/jellyfinwebsocket.cpp | 18 ++++ .../qml/components/LibraryItemDelegate.qml | 1 + sailfish/qml/components/PlayToolbar.qml | 4 +- sailfish/qml/components/VideoPlayer.qml | 2 +- sailfish/qml/pages/MainPage.qml | 4 +- sailfish/qml/pages/VideoPage.qml | 4 +- sailfish/qml/pages/itemdetails/SeasonPage.qml | 4 +- sailfish/qml/pages/itemdetails/SeriesPage.qml | 2 +- sailfish/qml/pages/itemdetails/VideoPage.qml | 6 +- 17 files changed, 304 insertions(+), 44 deletions(-) diff --git a/core/include/credentialmanager.h b/core/include/credentialmanager.h index 232f70a..9594671 100644 --- a/core/include/credentialmanager.h +++ b/core/include/credentialmanager.h @@ -127,4 +127,4 @@ private: QSettings m_settings; }; -#endif +#endif // CREDENTIAL_MANAGER_H diff --git a/core/include/jellyfinapiclient.h b/core/include/jellyfinapiclient.h index 4d8635a..23da369 100644 --- a/core/include/jellyfinapiclient.h +++ b/core/include/jellyfinapiclient.h @@ -38,6 +38,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include "credentialmanager.h" #include "jellyfindeviceprofile.h" +#include "jellyfinitem.h" #include "jellyfinwebsocket.h" namespace Jellyfin { @@ -144,6 +145,15 @@ signals: void itemFetched(const QString &itemId, const QJsonObject &result); void itemFetchFailed(const QString &itemId, const QNetworkReply::NetworkError error); + /** + * @brief onUserDataChanged Emitted when the user data of an item is changed on the server. + * @param itemId The id of the item being changed + * @param userData The new user data + * + * Note: only Jellyfin::UserData should connect to this signal, they will update themselves! + */ + void userDataChanged(const QString &itemId, QSharedPointer userData); + public slots: /** * @brief Tries to access credentials and connect to a server. If nothing has been configured yet, @@ -172,6 +182,7 @@ public slots: protected slots: void defaultNetworkErrorHandler(QNetworkReply::NetworkError error); + void onUserDataChanged(const QString &itemId, QSharedPointer newData); protected: /** diff --git a/core/include/jellyfinapimodel.h b/core/include/jellyfinapimodel.h index fde3196..50dd4e5 100644 --- a/core/include/jellyfinapimodel.h +++ b/core/include/jellyfinapimodel.h @@ -129,7 +129,7 @@ public: * responseHasRecords should be true */ explicit ApiModel(QString path, bool responseHasRecords, bool passUserId = false, QObject *parent = nullptr); - Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient) + Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient NOTIFY apiClientChanged) Q_PROPERTY(ModelStatus status READ status NOTIFY statusChanged) // Query properties @@ -171,6 +171,7 @@ public: } signals: + void apiClientChanged(ApiClient *newApiClient); void statusChanged(ModelStatus newStatus); void limitChanged(int newLimit); void parentIdChanged(QString newParentId); @@ -242,6 +243,9 @@ private: */ void generateFields(); QString sortByToString(SortOptions::SortBy sortBy); + + void convertToCamelCase(QJsonValueRef val); + QString convertToCamelCaseHelper(const QString &str); }; /** @@ -253,40 +257,53 @@ public: : ApiModel ("/users/public", false, false, parent) { } }; +/** + * @brief Base class for each model that works with items. + * + * Listens for updates in the library and updates the model accordingly. + */ +class ItemModel : public ApiModel { + Q_OBJECT +public: + explicit ItemModel (QString path, bool responseHasRecords, bool replaceUser, QObject *parent = nullptr); +public slots: + void onUserDataChanged(const QString &itemId, QSharedPointer userData); +}; + class UserViewModel : public ApiModel { public: explicit UserViewModel (QObject *parent = nullptr) : ApiModel ("/Users/{{user}}/Views", true, false, parent) {} }; -class UserItemModel : public ApiModel { +class UserItemModel : public ItemModel { public: explicit UserItemModel (QObject *parent = nullptr) - : ApiModel ("/Users/{{user}}/Items", true, false, parent) {} + : ItemModel ("/Users/{{user}}/Items", true, false, parent) {} }; -class UserItemResumeModel : public ApiModel { +class UserItemResumeModel : public ItemModel { public: explicit UserItemResumeModel (QObject *parent = nullptr) - : ApiModel ("/Users/{{user}}/Items/Resume", true, false, parent) {} + : ItemModel ("/Users/{{user}}/Items/Resume", true, false, parent) {} }; -class UserItemLatestModel : public ApiModel { +class UserItemLatestModel : public ItemModel { public: explicit UserItemLatestModel (QObject *parent = nullptr) - : ApiModel ("/Users/{{user}}/Items/Latest", false, false, parent) {} + : ItemModel ("/Users/{{user}}/Items/Latest", false, false, parent) {} }; -class ShowSeasonsModel : public ApiModel { +class ShowSeasonsModel : public ItemModel { public: explicit ShowSeasonsModel (QObject *parent = nullptr) - : ApiModel ("/Shows/{{show}}/Seasons", true, true, parent) {} + : ItemModel ("/Shows/{{show}}/Seasons", true, true, parent) {} }; -class ShowEpisodesModel : public ApiModel { +class ShowEpisodesModel : public ItemModel { public: explicit ShowEpisodesModel (QObject *parent = nullptr) - : ApiModel ("/Shows/{{show}}/Episodes", true, true, parent) {} + : ItemModel ("/Shows/{{show}}/Episodes", true, true, parent) {} }; diff --git a/core/include/jellyfinitem.h b/core/include/jellyfinitem.h index d970f6d..323bf5d 100644 --- a/core/include/jellyfinitem.h +++ b/core/include/jellyfinitem.h @@ -1,5 +1,24 @@ -#ifndef JELLYFINITEM_H -#define JELLYFINITEM_H +/* +Sailfin: a Jellyfin client written using Qt +Copyright (C) 2020 Chris Josten + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef JELLYFIN_ITEM_H +#define JELLYFIN_ITEM_H #include #include @@ -23,7 +42,7 @@ #include "jellyfinapiclient.h" namespace Jellyfin { - +class ApiClient; /** * @brief Base class for a serializable object. * @@ -40,7 +59,7 @@ public: * @param obj The data to load into this object. */ void deserialize(const QJsonObject &obj); - QJsonObject serialize() const; + QJsonObject serialize(bool capitalize = true) const; private: QVariant jsonToVariant(QMetaProperty prop, const QJsonValue &val, const QJsonObject &root); QJsonValue variantToJson(const QVariant var) const; @@ -56,7 +75,7 @@ private: * @param str The string to modify * @return THe modified string */ - static QString toPascalCase(QString str); + static QString toPascalCase(QString st); static const QRegularExpression m_listExpression; /** @@ -157,6 +176,54 @@ private: int m_index = -1; }; +class UserData : public JsonSerializable { + Q_OBJECT +public: + Q_INVOKABLE explicit UserData(QObject *parent = nullptr); + + Q_PROPERTY(double playedPercentage READ playedPercentage WRITE setPlayedPercentage RESET resetPlayedPercentage NOTIFY playedPercentageChanged) + Q_PROPERTY(qint64 playbackPositionTicks READ playbackPositionTicks WRITE setPlaybackPositionTicks NOTIFY playbackPositionTicksChanged) + Q_PROPERTY(bool isFavorite READ isFavorite WRITE setIsFavorite NOTIFY isFavoriteChanged) + Q_PROPERTY(bool likes READ likes WRITE setLikes RESET resetLikes NOTIFY likesChanged) + Q_PROPERTY(bool played READ played WRITE setPlayed NOTIFY playedChanged) + Q_PROPERTY(QString itemId READ itemId MEMBER m_itemId); + + double playedPercentage() const { return m_playedPercentage.value_or(0.0); } + void setPlayedPercentage(double newPlayedPercentage) { m_playedPercentage = newPlayedPercentage; emit playedPercentageChanged(newPlayedPercentage); } + void resetPlayedPercentage() { m_playedPercentage = std::nullopt; emit playedPercentageChanged(0.0); updateOnServer(); } + + qint64 playbackPositionTicks() const { return m_playbackPositionTicks; } + void setPlaybackPositionTicks(qint64 newPlaybackPositionTicks) { m_playbackPositionTicks = newPlaybackPositionTicks; emit playbackPositionTicksChanged(newPlaybackPositionTicks); } + + bool played() const { return m_played; } + void setPlayed(bool newPlayed) { m_played = newPlayed; emit playedChanged(newPlayed); updateOnServer(); } + + bool likes() const { return m_likes.value_or(false); } + void setLikes(bool newLikes) { m_likes = newLikes; emit likesChanged(newLikes); } + void resetLikes() { m_likes = std::nullopt; emit likesChanged(false); updateOnServer(); } + + bool isFavorite() const { return m_isFavorite; } + void setIsFavorite(bool newIsFavorite) { m_isFavorite = newIsFavorite; emit isFavoriteChanged(newIsFavorite); updateOnServer(); } + + const QString &itemId() const { return m_itemId; } +signals: + void playedPercentageChanged(double newPlayedPercentage); + void playbackPositionTicksChanged(qint64 playbackPositionTicks); + void isFavoriteChanged(bool newIsFavorite); + void likesChanged(bool newLikes); + void playedChanged(bool newPlayed); +public slots: + void updateOnServer(); + void onUpdated(QSharedPointer other); +private: + std::optional m_playedPercentage = std::nullopt; + qint64 m_playbackPositionTicks = 0; + bool m_isFavorite = false; + std::optional m_likes = std::nullopt; + bool m_played; + QString m_itemId; +}; + class Item : public RemoteData { Q_OBJECT public: @@ -206,6 +273,7 @@ public: Q_PROPERTY(int indexNumberEnd READ indexNumberEnd WRITE setIndexNumberEnd NOTIFY indexNumberEndChanged) Q_PROPERTY(bool isFolder READ isFolder WRITE setIsFolder NOTIFY isFolderChanged) Q_PROPERTY(QString type MEMBER m_type NOTIFY typeChanged) + Q_PROPERTY(UserData *userData MEMBER m_userData NOTIFY userDataChanged) Q_PROPERTY(QString seriesName MEMBER m_seriesName NOTIFY seriesNameChanged) Q_PROPERTY(QString seasonName MEMBER m_seasonName NOTIFY seasonNameChanged) Q_PROPERTY(QList __list__mediaStreams MEMBER __list__m_mediaStreams NOTIFY mediaStreamsChanged) @@ -283,6 +351,7 @@ signals: void indexNumberEndChanged(int newIndexNumberEnd); void isFolderChanged(bool newIsFolder); void typeChanged(const QString &newType); + void userDataChanged(UserData *newUserData); void seriesNameChanged(const QString &newSeriesName); void seasonNameChanged(const QString &newSeasonName); void mediaStreamsChanged(/*const QList &newMediaStreams*/); @@ -292,6 +361,7 @@ public slots: * @brief (Re)loads the item from the Jellyfin server. */ void reload() override; + void onUserDataChanged(const QString &itemId, QSharedPointer userData); protected: QString m_id; QString m_name; @@ -327,6 +397,7 @@ protected: std::optional m_indexNumberEnd = std::nullopt; std::optional m_isFolder = std::nullopt; QString m_type; + UserData *m_userData = nullptr; QString m_seriesName; QString m_seasonName; QList __list__m_mediaStreams; @@ -351,4 +422,4 @@ protected: void registerSerializableJsonTypes(const char* URI); } -#endif // JELLYFINITEM_H +#endif // JELLYFIN_ITEM_H diff --git a/core/include/jellyfinwebsocket.h b/core/include/jellyfinwebsocket.h index 781ec88..dd7317d 100644 --- a/core/include/jellyfinwebsocket.h +++ b/core/include/jellyfinwebsocket.h @@ -32,9 +32,17 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include #include "jellyfinapiclient.h" +#include "jellyfinitem.h" namespace Jellyfin { class ApiClient; +class UserData; +/** + * @brief Keeps a connection with the Jellyfin server to receive real time updates. + * + * This class will parse these messages and send them to ApiClient, which will emit signals for + * the interested classes. + */ class WebSocket : public QObject { Q_OBJECT public: @@ -47,7 +55,8 @@ public: explicit WebSocket(ApiClient *client); enum MessageType { ForceKeepAlive, - KeepAlive + KeepAlive, + UserDataChanged }; Q_ENUM(MessageType) public slots: diff --git a/core/src/jellyfinapiclient.cpp b/core/src/jellyfinapiclient.cpp index ef1ab01..b3f6c47 100644 --- a/core/src/jellyfinapiclient.cpp +++ b/core/src/jellyfinapiclient.cpp @@ -275,6 +275,10 @@ void ApiClient::defaultNetworkErrorHandler(QNetworkReply::NetworkError error) { rep->deleteLater(); } +void ApiClient::onUserDataChanged(const QString &itemId, QSharedPointer userData) { + userDataChanged(itemId, userData); +} + void ApiClient::setAuthenticated(bool authenticated) { this->m_authenticated = authenticated; if (authenticated) m_webSocket->open(); diff --git a/core/src/jellyfinapimodel.cpp b/core/src/jellyfinapimodel.cpp index f32a07a..25c5efd 100644 --- a/core/src/jellyfinapimodel.cpp +++ b/core/src/jellyfinapimodel.cpp @@ -125,6 +125,9 @@ void ApiModel::load(LoadType type) { case LOAD_MORE: this->beginInsertRows(QModelIndex(), m_array.size(), m_array.size() + items.size() - 1); // QJsonArray apparently doesn't allow concatenating lists like QList or std::vector + for (auto it = items.begin(); it != items.end(); it++) { + convertToCamelCase(*it); + } foreach (const QJsonValue &val, items) { m_array.append(val); } @@ -159,9 +162,11 @@ void ApiModel::generateFields() { QByteArray keyArr = keyName.toUtf8(); if (!m_roles.values().contains(keyArr)) { m_roles.insert(i++, keyArr); - //qDebug() << m_path << " adding " << keyName << " as " << ( i - 1); } } + for (auto it = m_array.begin(); it != m_array.end(); it++){ + convertToCamelCase(*it); + } this->endResetModel(); } @@ -174,8 +179,7 @@ QVariant ApiModel::data(const QModelIndex &index, int role) const { QJsonObject obj = m_array.at(index.row()).toObject(); - QString key = m_roles[role]; - key[0] = key[0].toUpper(); + const QString &key = m_roles[role]; if (obj.contains(key)) { return obj[key].toVariant(); } @@ -208,6 +212,64 @@ void ApiModel::fetchMore(const QModelIndex &parent) { void ApiModel::addQueryParameters(QUrlQuery &query) { Q_UNUSED(query)} +void ApiModel::convertToCamelCase(QJsonValueRef val) { + switch(val.type()) { + case QJsonValue::Object: { + QJsonObject obj = val.toObject(); + for(const QString &key: obj.keys()) { + QJsonValueRef ref = obj[key]; + convertToCamelCase(ref); + obj[convertToCamelCaseHelper(key)] = ref; + obj.remove(key); + } + val = obj; + break; + } + case QJsonValue::Array: { + QJsonArray arr = val.toArray(); + for (auto it = arr.begin(); it != arr.end(); it++) { + convertToCamelCase(*it); + } + val = arr; + break; + } + default: + break; + } +} + +QString ApiModel::convertToCamelCaseHelper(const QString &str) { + QString res(str); + res[0] = res[0].toLower(); + return res; +} + + +// Itemmodel + +ItemModel::ItemModel(QString path, bool hasRecordFields, bool replaceUser, QObject *parent) + : ApiModel (path, hasRecordFields, replaceUser, parent){ + connect(this, &ApiModel::apiClientChanged, this, [this](ApiClient *newApiClient) { + connect(newApiClient, &ApiClient::userDataChanged, this, &ItemModel::onUserDataChanged); + }); +} + +void ItemModel::onUserDataChanged(const QString &itemId, QSharedPointer userData) { + int i = 0; + for (QJsonValueRef val: m_array) { + QJsonObject item = val.toObject(); + if (item.contains("id") && item["id"].toString() == itemId) { + if (item.contains("userData")) { + QModelIndex cell = this->index(i); + item["userData"] = userData->serialize(false); + val = item; + this->dataChanged(cell, cell); + } + } + i++; + } +} + void registerModels(const char *URI) { qmlRegisterUncreatableType(URI, 1, 0, "ApiModel", "Is enum and base class"); diff --git a/core/src/jellyfinitem.cpp b/core/src/jellyfinitem.cpp index 20a91b1..a0cd6eb 100644 --- a/core/src/jellyfinitem.cpp +++ b/core/src/jellyfinitem.cpp @@ -1,3 +1,22 @@ +/* +Sailfin: a Jellyfin client written using Qt +Copyright (C) 2020 Chris Josten + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + #include "jellyfinitem.h" namespace Jellyfin { @@ -15,7 +34,6 @@ void JsonSerializable::deserialize(const QJsonObject &jObj) { if (!prop.isStored()) continue; if (!prop.isWritable()) continue; - qDebug() << toPascalCase(prop.name()); // Hardcoded exception for the property id, since its special inside QML if (QString(prop.name()) == "jellyfinId" && jObj.contains("Id")) { QJsonValue val = jObj["Id"]; @@ -30,17 +48,13 @@ void JsonSerializable::deserialize(const QJsonObject &jObj) { // the actual QList, so that qml can access the object with its real name. QString realName = toPascalCase(prop.name() + 8); if (!jObj.contains(realName)) { - qDebug() << "Ignoring " << realName << " - " << prop.name(); continue; } QJsonValue val = jObj[realName]; - qDebug() << realName << " - " << prop.name() << ": " << val; QMetaProperty realProp = obj->property(obj->indexOfProperty(prop.name() + 8)); if (!realProp.write(this, jsonToVariant(prop, val, jObj))) { qDebug() << "Write to " << prop.name() << "failed"; }; - } else { - qDebug() << "Ignored " << prop.name() << " while deserializing"; } } } @@ -51,19 +65,24 @@ QVariant JsonSerializable::jsonToVariant(QMetaProperty prop, const QJsonValue &v case QJsonValue::Undefined: return QVariant(); case QJsonValue::Bool: - case QJsonValue::Double: case QJsonValue::String: return val.toVariant(); + case QJsonValue::Double: + if (prop.type() == QVariant::LongLong) { + return static_cast(val.toDouble(-1)); + } if (prop.type() == QVariant::Int) { + return val.toInt(); + } else { + return val.toDouble(); + } case QJsonValue::Array: { QJsonArray arr = val.toArray(); QVariantList varArr; for (auto it = arr.constBegin(); it < arr.constEnd(); it++) { QVariant variant = jsonToVariant(prop, *it, root); - qDebug() << variant; varArr.append(variant); } - qDebug() << prop.name() << ": " << varArr.count(); return QVariant(varArr); } case QJsonValue::Object: @@ -104,7 +123,7 @@ QVariant JsonSerializable::jsonToVariant(QMetaProperty prop, const QJsonValue &v return QVariant(); } -QJsonObject JsonSerializable::serialize() const { +QJsonObject JsonSerializable::serialize(bool capitalize) const { QJsonObject result; const QMetaObject *obj = this->metaObject(); for (int i = 0; i < obj->propertyCount(); i++) { @@ -112,7 +131,7 @@ QJsonObject JsonSerializable::serialize() const { if (QString(prop.name()) == "jellyfinId") { result["Id"] = variantToJson(prop.read(this)); } else { - result[toPascalCase(prop.name())] = variantToJson(prop.read(this)); + result[capitalize ? toPascalCase(prop.name()) : prop.name()] = variantToJson(prop.read(this)); } } return result; @@ -206,9 +225,47 @@ bool MediaStream::operator==(const MediaStream &other) { && m_index == other.m_index; } +// UserData +UserData::UserData(QObject *parent) : JsonSerializable (parent) {} + +void UserData::updateOnServer() { + //TODO: implement +} + +void UserData::onUpdated(QSharedPointer other) { + // The reason I'm not using setLikes and similar is that they don't work with std::nullopt, + // since QML does not like it. + // THe other reason is that the setLikes method will send a post request to the server, to update the contents + // we don't want that to happen, obviously, since the application could end in an infinite loop. + if (this->m_playedPercentage != other->m_playedPercentage) { + this->m_playedPercentage = other->m_playedPercentage; + emit playedPercentageChanged(playedPercentage()); + } + if (m_playbackPositionTicks!= other->m_playbackPositionTicks) { + this->m_playbackPositionTicks = other->m_playbackPositionTicks; + emit playbackPositionTicksChanged(this->m_playbackPositionTicks); + } + if (m_isFavorite != other->m_isFavorite) { + this->m_isFavorite = other->m_isFavorite; + emit isFavoriteChanged(this->m_isFavorite); + } + if (this->m_likes != other->m_likes) { + this->m_likes = other->m_likes; + emit likesChanged(likes()); + } + if (this->m_played != other->m_played) { + this->m_played = other->m_played; + emit playedChanged(this->m_played); + } +} + // Item -Item::Item(QObject *parent) : RemoteData(parent) {} +Item::Item(QObject *parent) : RemoteData(parent) { + connect(this, &RemoteData::apiClientChanged, this, [this](ApiClient *newApiClient) { + connect(newApiClient, &ApiClient::userDataChanged, this, &Item::onUserDataChanged); + }); +} void Item::setJellyfinId(QString newId) { @@ -256,8 +313,14 @@ void Item::reload() { }); } +void Item::onUserDataChanged(const QString &itemId, QSharedPointer userData) { + if (itemId != m_id || m_userData == nullptr) return; + m_userData->onUpdated(userData); +} + void registerSerializableJsonTypes(const char* URI) { qmlRegisterType(URI, 1, 0, "MediaStream"); + qmlRegisterType(URI, 1, 0, "UserData"); qmlRegisterType(URI, 1, 0, "JellyfinItem"); } } diff --git a/core/src/jellyfinwebsocket.cpp b/core/src/jellyfinwebsocket.cpp index 44afcc5..940a377 100644 --- a/core/src/jellyfinwebsocket.cpp +++ b/core/src/jellyfinwebsocket.cpp @@ -69,6 +69,9 @@ void WebSocket::textMessageReceived(const QString &message) { MessageType messageType = static_cast(QMetaEnum::fromType().keyToValue(messageTypeStr.toLatin1(), &ok)); if (!ok) { qWarning() << "Unknown message arrived: " << messageTypeStr; + if (messageRoot.contains("Data")) { + qDebug() << "with data: " << QJsonDocument(messageRoot["Data"].toObject()).toJson(); + } return; } @@ -82,6 +85,21 @@ void WebSocket::textMessageReceived(const QString &message) { case KeepAlive: //TODO: do something? break; + case UserDataChanged: { + QJsonObject data2 = data.toObject(); + if (data2["UserId"] != m_apiClient->userId()) { + qDebug() << "Received UserDataCHanged for other user"; + break; + } + QJsonArray userDataList = data2["UserDataList"].toArray(); + for (QJsonValue val: userDataList) { + QSharedPointer userData(new UserData, &QObject::deleteLater); + userData->deserialize(val.toObject()); + m_apiClient->onUserDataChanged(userData->itemId(), userData); + } + + } + break; } } diff --git a/sailfish/qml/components/LibraryItemDelegate.qml b/sailfish/qml/components/LibraryItemDelegate.qml index f00e1d2..5f98a88 100644 --- a/sailfish/qml/components/LibraryItemDelegate.qml +++ b/sailfish/qml/components/LibraryItemDelegate.qml @@ -86,5 +86,6 @@ BackgroundItem { height: Theme.paddingSmall color: Theme.highlightColor width: root.progress * parent.width + Behavior on width { SmoothedAnimation {} } } } diff --git a/sailfish/qml/components/PlayToolbar.qml b/sailfish/qml/components/PlayToolbar.qml index 8ab7622..faf1b6b 100644 --- a/sailfish/qml/components/PlayToolbar.qml +++ b/sailfish/qml/components/PlayToolbar.qml @@ -24,6 +24,7 @@ Column { property alias imageSource : playImage.source property real imageAspectRatio: 1.0 property real playProgress: 0.0 + property bool favourited: false signal playPressed(bool startFromBeginning) spacing: Theme.paddingLarge @@ -73,7 +74,8 @@ Column { } IconButton { id: favouriteButton - icon.source: "image://theme/icon-m-favorite" + icon.source: favourited ? "image://theme/icon-m-favorite-selected" + : "image://theme/icon-m-favorite" } } diff --git a/sailfish/qml/components/VideoPlayer.qml b/sailfish/qml/components/VideoPlayer.qml index 79ed01b..5029075 100644 --- a/sailfish/qml/components/VideoPlayer.qml +++ b/sailfish/qml/components/VideoPlayer.qml @@ -39,7 +39,7 @@ SilicaItem { readonly property bool hudVisible: !hud.hidden || player.error !== MediaPlayer.NoError property alias audioTrack: mediaSource.audioIndex property alias subtitleTrack: mediaSource.subtitleIndex - property int startTicks: 0 + property real startTicks: 0 // Force a Light on Dark theme since I doubt that there are persons who are willing to watch a Video // on a white background. diff --git a/sailfish/qml/pages/MainPage.qml b/sailfish/qml/pages/MainPage.qml index 1e08747..81e6d48 100644 --- a/sailfish/qml/pages/MainPage.qml +++ b/sailfish/qml/pages/MainPage.qml @@ -198,12 +198,12 @@ Page { delegate: LibraryItemDelegate { property string id: model.id title: model.name - poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags["Primary"], "Primary", {"maxHeight": height}) + poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags["primary"], "Primary", {"maxHeight": height}) /*model.imageTags["Primary"] ? ApiClient.baseUrl + "/Items/" + model.id + "/Images/Primary?maxHeight=" + height + "&tag=" + model.imageTags["Primary"] : ""*/ landscape: !Utils.usePortraitCover(collectionType) - progress: (typeof model.userData !== "undefined") ? model.userData.PlayedPercentage / 100 : 0.0 + progress: (typeof model.userData !== "undefined") ? model.userData.playedPercentage / 100 : 0.0 onClicked: { pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.id}) diff --git a/sailfish/qml/pages/VideoPage.qml b/sailfish/qml/pages/VideoPage.qml index 7d321f4..8f1d8bc 100644 --- a/sailfish/qml/pages/VideoPage.qml +++ b/sailfish/qml/pages/VideoPage.qml @@ -33,7 +33,7 @@ Page { property var itemData property int audioTrack property int subtitleTrack - property int startTicks: 0 + property real startTicks: 0 // Why is this a real? Because an integer only goes to 3:44 when the ticks are converted to doubles allowedOrientations: Orientation.All showNavigationIndicator: videoPlayer.hudVisible @@ -43,7 +43,7 @@ Page { anchors.fill: parent itemId: videoPage.itemId player: appWindow.mediaPlayer - title: itemData.Name + title: itemData.name audioTrack: videoPage.audioTrack subtitleTrack: videoPage.subtitleTrack startTicks: videoPage.startTicks diff --git a/sailfish/qml/pages/itemdetails/SeasonPage.qml b/sailfish/qml/pages/itemdetails/SeasonPage.qml index 6273512..649a8bc 100644 --- a/sailfish/qml/pages/itemdetails/SeasonPage.qml +++ b/sailfish/qml/pages/itemdetails/SeasonPage.qml @@ -53,7 +53,7 @@ BaseDetailPage { } width: Constants.libraryDelegateWidth height: Constants.libraryDelegateHeight - source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags["Primary"], "Primary", {"maxHeight": height}) + source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags.primary, "Primary", {"maxHeight": height}) fillMode: Image.PreserveAspectCrop clip: true @@ -78,7 +78,7 @@ BaseDetailPage { bottom: parent.bottom } height: Theme.paddingMedium - width: model.userData.PlayedPercentage * parent.width / 100 + width: model.userData.playedPercentage * parent.width / 100 color: Theme.highlightColor } } diff --git a/sailfish/qml/pages/itemdetails/SeriesPage.qml b/sailfish/qml/pages/itemdetails/SeriesPage.qml index 0943886..461a461 100644 --- a/sailfish/qml/pages/itemdetails/SeriesPage.qml +++ b/sailfish/qml/pages/itemdetails/SeriesPage.qml @@ -79,7 +79,7 @@ BaseDetailPage { leftMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin delegate: LibraryItemDelegate { - poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags["Primary"], "Primary", {"maxHeight": height}) + poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags.primary, "Primary", {"maxHeight": height}) title: model.name onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.id}) } diff --git a/sailfish/qml/pages/itemdetails/VideoPage.qml b/sailfish/qml/pages/itemdetails/VideoPage.qml index a3feac2..6009daa 100644 --- a/sailfish/qml/pages/itemdetails/VideoPage.qml +++ b/sailfish/qml/pages/itemdetails/VideoPage.qml @@ -32,6 +32,7 @@ import "../.." BaseDetailPage { property alias subtitle: pageHeader.description default property alias _data: content.data + property real _playbackProsition: itemData.userData.playbackPositionTicks SilicaFlickable { anchors.fill: parent contentHeight: content.height + Theme.paddingLarge @@ -54,13 +55,14 @@ BaseDetailPage { width: parent.width imageSource: Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width}) imageAspectRatio: Constants.horizontalVideoAspectRatio - playProgress: itemData.UserData.PlayedPercentage / 100 + favourited: itemData.userData.isFavorite + playProgress: itemData.userData.playedPercentage / 100 onPlayPressed: pageStack.push(Qt.resolvedUrl("../VideoPage.qml"), {"itemId": itemId, "itemData": itemData, "audioTrack": trackSelector.audioTrack, "subtitleTrack": trackSelector.subtitleTrack, "startTicks": startFromBeginning ? 0.0 - : itemData.UserData.PlaybackPositionTicks }) + : _playbackProsition }) } VideoTrackSelector {