From 8a683df2a2a10d5542dbc56460b0d770a80ee060 Mon Sep 17 00:00:00 2001 From: Chris Josten Date: Sat, 10 Oct 2020 14:30:49 +0200 Subject: [PATCH] Added more fields to Jellyfin::Item, update qml * [UI] Improved: series season page now shows favourite and watched marks Refractored some more QML to support camelCase items --- core/core.pro | 2 + core/include/jellyfinapimodel.h | 4 +- core/include/jellyfinitem.h | 10 +++++ core/include/jsonhelper.h | 21 ++++++++++ core/src/jellyfinapimodel.cpp | 34 +--------------- core/src/jellyfinitem.cpp | 16 ++++++-- core/src/jsonhelper.cpp | 40 +++++++++++++++++++ sailfish/qml/cover/CoverPage.qml | 4 +- sailfish/qml/cover/PosterCover.qml | 15 ++++--- sailfish/qml/cover/VideoCover.qml | 20 +++------- .../qml/pages/itemdetails/BaseDetailPage.qml | 2 +- .../qml/pages/itemdetails/CollectionPage.qml | 4 +- sailfish/qml/pages/itemdetails/SeasonPage.qml | 29 +++++++++++--- 13 files changed, 131 insertions(+), 70 deletions(-) create mode 100644 core/include/jsonhelper.h create mode 100644 core/src/jsonhelper.cpp diff --git a/core/core.pro b/core/core.pro index d631cdd..50e52b0 100644 --- a/core/core.pro +++ b/core/core.pro @@ -13,6 +13,7 @@ SOURCES += \ src/jellyfinitem.cpp \ src/jellyfinplaybackmanager.cpp \ src/jellyfinwebsocket.cpp \ + src/jsonhelper.cpp \ src/serverdiscoverymodel.cpp HEADERS += \ @@ -24,6 +25,7 @@ HEADERS += \ include/jellyfinitem.h \ include/jellyfinplaybackmanager.h \ include/jellyfinwebsocket.h \ + include/jsonhelper.h \ include/serverdiscoverymodel.h VERSION = $$SAILFIN_VERSION diff --git a/core/include/jellyfinapimodel.h b/core/include/jellyfinapimodel.h index 50dd4e5..6257ff3 100644 --- a/core/include/jellyfinapimodel.h +++ b/core/include/jellyfinapimodel.h @@ -30,6 +30,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include #include "jellyfinapiclient.h" +#include "jsonhelper.h" namespace Jellyfin { class SortOptions : public QObject{ @@ -243,9 +244,6 @@ private: */ void generateFields(); QString sortByToString(SortOptions::SortBy sortBy); - - void convertToCamelCase(QJsonValueRef val); - QString convertToCamelCaseHelper(const QString &str); }; /** diff --git a/core/include/jellyfinitem.h b/core/include/jellyfinitem.h index 323bf5d..4605635 100644 --- a/core/include/jellyfinitem.h +++ b/core/include/jellyfinitem.h @@ -40,6 +40,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include #include "jellyfinapiclient.h" +#include "jsonhelper.h" namespace Jellyfin { class ApiClient; @@ -63,6 +64,7 @@ public: private: QVariant jsonToVariant(QMetaProperty prop, const QJsonValue &val, const QJsonObject &root); QJsonValue variantToJson(const QVariant var) const; + QVariant deserializeQobject(const QJsonObject &obj, const QMetaProperty &prop); /** * @brief Sets the first letter of the string to lower case (to make it camelCase). @@ -278,6 +280,10 @@ public: Q_PROPERTY(QString seasonName MEMBER m_seasonName NOTIFY seasonNameChanged) Q_PROPERTY(QList __list__mediaStreams MEMBER __list__m_mediaStreams NOTIFY mediaStreamsChanged) Q_PROPERTY(QVariantList mediaStreams MEMBER m_mediaStreams NOTIFY mediaStreamsChanged STORED false) + // Why is this a QJsonObject? Well, because I couldn't be bothered to implement the deserialisations of + // a QHash at the moment. + Q_PROPERTY(QJsonObject imageTags MEMBER m_imageTags NOTIFY imageTagsChanged) + Q_PROPERTY(QJsonObject imageBlurHashes MEMBER m_imageBlurHashes NOTIFY imageBlurHashesChanged) QString jellyfinId() const { return m_id; } void setJellyfinId(QString newId); @@ -355,6 +361,8 @@ signals: void seriesNameChanged(const QString &newSeriesName); void seasonNameChanged(const QString &newSeasonName); void mediaStreamsChanged(/*const QList &newMediaStreams*/); + void imageTagsChanged(); + void imageBlurHashesChanged(); public slots: /** @@ -402,6 +410,8 @@ protected: QString m_seasonName; QList __list__m_mediaStreams; QVariantList m_mediaStreams; + QJsonObject m_imageTags; + QJsonObject m_imageBlurHashes; template QQmlListProperty toReadOnlyQmlListProperty(QList &list) { diff --git a/core/include/jsonhelper.h b/core/include/jsonhelper.h new file mode 100644 index 0000000..8ea5fe5 --- /dev/null +++ b/core/include/jsonhelper.h @@ -0,0 +1,21 @@ +#ifndef JSON_SERIALIZER_H +#define JSON_SERIALIZER_H + + +#include +#include +#include +#include +#include +#include + +namespace Jellyfin { + +namespace JsonHelper { + void convertToCamelCase(QJsonValueRef val); + QString convertToCamelCaseHelper(const QString &str); +}; + +} + +#endif // JSONSERIALIZER_H diff --git a/core/src/jellyfinapimodel.cpp b/core/src/jellyfinapimodel.cpp index 25c5efd..6e1ae05 100644 --- a/core/src/jellyfinapimodel.cpp +++ b/core/src/jellyfinapimodel.cpp @@ -126,7 +126,7 @@ void ApiModel::load(LoadType type) { 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); + JsonHelper::convertToCamelCase(*it); } foreach (const QJsonValue &val, items) { m_array.append(val); @@ -165,7 +165,7 @@ void ApiModel::generateFields() { } } for (auto it = m_array.begin(); it != m_array.end(); it++){ - convertToCamelCase(*it); + JsonHelper::convertToCamelCase(*it); } this->endResetModel(); } @@ -212,37 +212,7 @@ 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 diff --git a/core/src/jellyfinitem.cpp b/core/src/jellyfinitem.cpp index a0cd6eb..888dbef 100644 --- a/core/src/jellyfinitem.cpp +++ b/core/src/jellyfinitem.cpp @@ -87,7 +87,19 @@ QVariant JsonSerializable::jsonToVariant(QMetaProperty prop, const QJsonValue &v } case QJsonValue::Object: QJsonObject innerObj = val.toObject(); - int typeNo = prop.userType(); + if (prop.userType() == QMetaType::QJsonObject) { + QJsonArray tmp = {innerObj }; + JsonHelper::convertToCamelCase(QJsonValueRef(&tmp, 0)); + return QVariant(innerObj); + } else { + return deserializeQobject(innerObj, prop); + } + } + return QVariant(); +} + +QVariant JsonSerializable::deserializeQobject(const QJsonObject &innerObj, const QMetaProperty &prop) { + int typeNo = prop.userType(); const QMetaObject *metaType = QMetaType::metaObjectForType(prop.userType()); if (metaType == nullptr) { // Try to determine if the type is a qlist @@ -119,8 +131,6 @@ QVariant JsonSerializable::jsonToVariant(QMetaProperty prop, const QJsonValue &v qDebug() << "Object is not a serializable one!"; return QVariant(); } - } - return QVariant(); } QJsonObject JsonSerializable::serialize(bool capitalize) const { diff --git a/core/src/jsonhelper.cpp b/core/src/jsonhelper.cpp new file mode 100644 index 0000000..60f29a6 --- /dev/null +++ b/core/src/jsonhelper.cpp @@ -0,0 +1,40 @@ +#include "jsonhelper.h" + +namespace Jellyfin { + +namespace JsonHelper { + +void 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 convertToCamelCaseHelper(const QString &str) { + QString res(str); + res[0] = res[0].toLower(); + return res; +} + +} // NS JsonHelper +} // NS Jellyfin diff --git a/sailfish/qml/cover/CoverPage.qml b/sailfish/qml/cover/CoverPage.qml index dba0a4a..fbbdac2 100644 --- a/sailfish/qml/cover/CoverPage.qml +++ b/sailfish/qml/cover/CoverPage.qml @@ -79,7 +79,7 @@ CoverBackground { clip: true height: row1.height width: height - source: model.id ? Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags["Primary"], "Primary", {"maxHeight": row1.height}) + source: model.id ? Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags.primary, "Primary", {"maxHeight": row1.height}) : "" fillMode: Image.PreserveAspectCrop } @@ -123,7 +123,7 @@ CoverBackground { clip: true height: row2.height width: height - source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags["Primary"], "Primary", {"maxHeight": row1.height}) + source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags.primary, "Primary", {"maxHeight": row1.height}) fillMode: Image.PreserveAspectCrop } } diff --git a/sailfish/qml/cover/PosterCover.qml b/sailfish/qml/cover/PosterCover.qml index d8a032d..74761d4 100644 --- a/sailfish/qml/cover/PosterCover.qml +++ b/sailfish/qml/cover/PosterCover.qml @@ -29,16 +29,15 @@ CoverBackground { property var mData: appWindow.itemData RemoteImage { anchors.fill: parent - source: mData.ImageTags["Primary"] ? ApiClient.baseUrl + "/Items/" + mData.Id - + "/Images/Primary?maxHeight=" + height + "&tag=" + mData.ImageTags["Primary"] - : "" + source: Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width}) fillMode: Image.PreserveAspectCrop + onSourceChanged: console.log(source) } Shim { // Movies usually show their name on the poster, // so showing it here as well is a bit double - visible: itemData.Type !== "Movie" + visible: itemData.type !== "Movie" anchors { left: parent.left right: parent.right @@ -52,7 +51,7 @@ CoverBackground { top: parent.top left: parent.left } - width: itemData.UserData.PlayedPercentage / 100 * parent.width + width: itemData.userData.playedPercentage / 100 * parent.width height: Theme.paddingSmall color: Theme.highlightColor } @@ -72,13 +71,13 @@ CoverBackground { right: parent.right } color: Theme.primaryColor - text: itemData.Name + text: itemData.name truncationMode: TruncationMode.Fade } Label { - visible: typeof itemData.RunTimeTicks !== "undefined" + visible: typeof itemData.runTimeTicks !== "undefined" color: Theme.secondaryColor - text: Utils.ticksToText(itemData.RunTimeTicks) + text: Utils.ticksToText(itemData.runTimeTicks) } } } diff --git a/sailfish/qml/cover/VideoCover.qml b/sailfish/qml/cover/VideoCover.qml index 898861f..49c4304 100644 --- a/sailfish/qml/cover/VideoCover.qml +++ b/sailfish/qml/cover/VideoCover.qml @@ -25,30 +25,22 @@ import nl.netsoj.chris.Jellyfin 1.0 import "../components" -CoverBackground { +PosterCover { readonly property MediaPlayer player: appWindow.mediaPlayer property var mData: appWindow.itemData - Rectangle { + // Wanted to display the currently running move on here, but it's hard :/ + /*Rectangle { anchors.fill: parent color: "black" - // Wanted to display the currently running move on here, but it's hard :/ - /*VideoOutput { + VideoOutput { id: coverOutput anchors.fill: parent source: player - }*/ + } - } - // As a temporary fallback, use the poster image - RemoteImage { - anchors.fill: parent - source: mData.ImageTags["Primary"] ? ApiClient.baseUrl + "/Items/" + mData.Id - + "/Images/Primary?maxHeight=" + height + "&tag=" + mData.ImageTags["Primary"] - : "" - fillMode: Image.PreserveAspectCrop - } + }*/ Shim { anchors { diff --git a/sailfish/qml/pages/itemdetails/BaseDetailPage.qml b/sailfish/qml/pages/itemdetails/BaseDetailPage.qml index a0b9b5b..9b79a46 100644 --- a/sailfish/qml/pages/itemdetails/BaseDetailPage.qml +++ b/sailfish/qml/pages/itemdetails/BaseDetailPage.qml @@ -83,7 +83,7 @@ Page { //appWindow.itemData = ({}) } if (status == PageStatus.Active) { - + appWindow.itemData = jItem } } } diff --git a/sailfish/qml/pages/itemdetails/CollectionPage.qml b/sailfish/qml/pages/itemdetails/CollectionPage.qml index 47eacc5..506d9c8 100644 --- a/sailfish/qml/pages/itemdetails/CollectionPage.qml +++ b/sailfish/qml/pages/itemdetails/CollectionPage.qml @@ -43,7 +43,7 @@ BaseDetailPage { cellHeight: Utils.usePortraitCover(itemData.CollectionType) ? Constants.libraryDelegatePosterHeight : Constants.libraryDelegateHeight header: PageHeader { - title: itemData.Name || qsTr("Loading") + title: itemData.name || qsTr("Loading") } PullDownMenu { id: downMenu @@ -58,7 +58,7 @@ BaseDetailPage { RemoteImage { id: itemImage anchors.fill: parent - source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags["Primary"], "Primary", {"maxWidth": width}) + source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags.primary, "Primary", {"maxWidth": width}) fallbackColor: Utils.colorFromString(model.name) fillMode: Image.PreserveAspectCrop clip: true diff --git a/sailfish/qml/pages/itemdetails/SeasonPage.qml b/sailfish/qml/pages/itemdetails/SeasonPage.qml index 649a8bc..280df14 100644 --- a/sailfish/qml/pages/itemdetails/SeasonPage.qml +++ b/sailfish/qml/pages/itemdetails/SeasonPage.qml @@ -68,7 +68,7 @@ BaseDetailPage { shimColor: Theme.overlayBackgroundColor shimOpacity: Theme.opacityOverlay //width: model.userData.PlayedPercentage * parent.width / 100 - visible: episodeProgress.width > 0 // It doesn't look nice when it's visible on every image + visible: episodeProgress.width > 0 || model.userData.played || model.userData.isFavorite // It doesn't look nice when it's visible on every image } Rectangle { @@ -78,9 +78,28 @@ BaseDetailPage { bottom: parent.bottom } height: Theme.paddingMedium - width: model.userData.playedPercentage * parent.width / 100 + width: model.userData.playedPercentage * parent.width / 100 color: Theme.highlightColor } + Row { + spacing: Theme.paddingSmall + anchors { + bottom: episodeProgress.width > 0 ? episodeProgress.top : parent.bottom + bottomMargin: Theme.paddingMedium + right: parent.right + rightMargin: Theme.paddingMedium + } + + Icon { + source: "image://theme/icon-s-checkmark" + visible: model.userData.played + } + + Icon { + source: "image://theme/icon-s-favorite" + visible: model.userData.isFavorite + } + } } Label { @@ -129,9 +148,9 @@ BaseDetailPage { } onStatusChanged: { if (status == PageStatus.Active) { - console.log(JSON.stringify(itemData)) - episodeModel.show = itemData.seriesId - episodeModel.seasonId = itemData.jellyfinId + //console.log(JSON.stringify(itemData)) + episodeModel.show = itemData.seriesId + episodeModel.seasonId = itemData.jellyfinId } } }