1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-11-22 09:15:18 +00:00

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
This commit is contained in:
Chris Josten 2020-10-10 14:30:49 +02:00
parent d81fa50715
commit 8a683df2a2
13 changed files with 131 additions and 70 deletions

View file

@ -13,6 +13,7 @@ SOURCES += \
src/jellyfinitem.cpp \ src/jellyfinitem.cpp \
src/jellyfinplaybackmanager.cpp \ src/jellyfinplaybackmanager.cpp \
src/jellyfinwebsocket.cpp \ src/jellyfinwebsocket.cpp \
src/jsonhelper.cpp \
src/serverdiscoverymodel.cpp src/serverdiscoverymodel.cpp
HEADERS += \ HEADERS += \
@ -24,6 +25,7 @@ HEADERS += \
include/jellyfinitem.h \ include/jellyfinitem.h \
include/jellyfinplaybackmanager.h \ include/jellyfinplaybackmanager.h \
include/jellyfinwebsocket.h \ include/jellyfinwebsocket.h \
include/jsonhelper.h \
include/serverdiscoverymodel.h include/serverdiscoverymodel.h
VERSION = $$SAILFIN_VERSION VERSION = $$SAILFIN_VERSION

View file

@ -30,6 +30,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include <QVariant> #include <QVariant>
#include "jellyfinapiclient.h" #include "jellyfinapiclient.h"
#include "jsonhelper.h"
namespace Jellyfin { namespace Jellyfin {
class SortOptions : public QObject{ class SortOptions : public QObject{
@ -243,9 +244,6 @@ private:
*/ */
void generateFields(); void generateFields();
QString sortByToString(SortOptions::SortBy sortBy); QString sortByToString(SortOptions::SortBy sortBy);
void convertToCamelCase(QJsonValueRef val);
QString convertToCamelCaseHelper(const QString &str);
}; };
/** /**

View file

@ -40,6 +40,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include <cmath> #include <cmath>
#include "jellyfinapiclient.h" #include "jellyfinapiclient.h"
#include "jsonhelper.h"
namespace Jellyfin { namespace Jellyfin {
class ApiClient; class ApiClient;
@ -63,6 +64,7 @@ public:
private: private:
QVariant jsonToVariant(QMetaProperty prop, const QJsonValue &val, const QJsonObject &root); QVariant jsonToVariant(QMetaProperty prop, const QJsonValue &val, const QJsonObject &root);
QJsonValue variantToJson(const QVariant var) const; 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). * @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(QString seasonName MEMBER m_seasonName NOTIFY seasonNameChanged)
Q_PROPERTY(QList<MediaStream *> __list__mediaStreams MEMBER __list__m_mediaStreams NOTIFY mediaStreamsChanged) Q_PROPERTY(QList<MediaStream *> __list__mediaStreams MEMBER __list__m_mediaStreams NOTIFY mediaStreamsChanged)
Q_PROPERTY(QVariantList mediaStreams MEMBER m_mediaStreams NOTIFY mediaStreamsChanged STORED false) 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; } QString jellyfinId() const { return m_id; }
void setJellyfinId(QString newId); void setJellyfinId(QString newId);
@ -355,6 +361,8 @@ signals:
void seriesNameChanged(const QString &newSeriesName); void seriesNameChanged(const QString &newSeriesName);
void seasonNameChanged(const QString &newSeasonName); void seasonNameChanged(const QString &newSeasonName);
void mediaStreamsChanged(/*const QList<MediaStream *> &newMediaStreams*/); void mediaStreamsChanged(/*const QList<MediaStream *> &newMediaStreams*/);
void imageTagsChanged();
void imageBlurHashesChanged();
public slots: public slots:
/** /**
@ -402,6 +410,8 @@ protected:
QString m_seasonName; QString m_seasonName;
QList<MediaStream *> __list__m_mediaStreams; QList<MediaStream *> __list__m_mediaStreams;
QVariantList m_mediaStreams; QVariantList m_mediaStreams;
QJsonObject m_imageTags;
QJsonObject m_imageBlurHashes;
template<typename T> template<typename T>
QQmlListProperty<T> toReadOnlyQmlListProperty(QList<T *> &list) { QQmlListProperty<T> toReadOnlyQmlListProperty(QList<T *> &list) {

21
core/include/jsonhelper.h Normal file
View file

@ -0,0 +1,21 @@
#ifndef JSON_SERIALIZER_H
#define JSON_SERIALIZER_H
#include <QList>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QJsonValueRef>
#include <QString>
namespace Jellyfin {
namespace JsonHelper {
void convertToCamelCase(QJsonValueRef val);
QString convertToCamelCaseHelper(const QString &str);
};
}
#endif // JSONSERIALIZER_H

View file

@ -126,7 +126,7 @@ void ApiModel::load(LoadType type) {
this->beginInsertRows(QModelIndex(), m_array.size(), m_array.size() + items.size() - 1); this->beginInsertRows(QModelIndex(), m_array.size(), m_array.size() + items.size() - 1);
// QJsonArray apparently doesn't allow concatenating lists like QList or std::vector // QJsonArray apparently doesn't allow concatenating lists like QList or std::vector
for (auto it = items.begin(); it != items.end(); it++) { for (auto it = items.begin(); it != items.end(); it++) {
convertToCamelCase(*it); JsonHelper::convertToCamelCase(*it);
} }
foreach (const QJsonValue &val, items) { foreach (const QJsonValue &val, items) {
m_array.append(val); m_array.append(val);
@ -165,7 +165,7 @@ void ApiModel::generateFields() {
} }
} }
for (auto it = m_array.begin(); it != m_array.end(); it++){ for (auto it = m_array.begin(); it != m_array.end(); it++){
convertToCamelCase(*it); JsonHelper::convertToCamelCase(*it);
} }
this->endResetModel(); this->endResetModel();
} }
@ -212,37 +212,7 @@ void ApiModel::fetchMore(const QModelIndex &parent) {
void ApiModel::addQueryParameters(QUrlQuery &query) { Q_UNUSED(query)} 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

View file

@ -87,7 +87,19 @@ QVariant JsonSerializable::jsonToVariant(QMetaProperty prop, const QJsonValue &v
} }
case QJsonValue::Object: case QJsonValue::Object:
QJsonObject innerObj = val.toObject(); 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()); const QMetaObject *metaType = QMetaType::metaObjectForType(prop.userType());
if (metaType == nullptr) { if (metaType == nullptr) {
// Try to determine if the type is a qlist // 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!"; qDebug() << "Object is not a serializable one!";
return QVariant(); return QVariant();
} }
}
return QVariant();
} }
QJsonObject JsonSerializable::serialize(bool capitalize) const { QJsonObject JsonSerializable::serialize(bool capitalize) const {

40
core/src/jsonhelper.cpp Normal file
View file

@ -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

View file

@ -79,7 +79,7 @@ CoverBackground {
clip: true clip: true
height: row1.height height: row1.height
width: 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 fillMode: Image.PreserveAspectCrop
} }
@ -123,7 +123,7 @@ CoverBackground {
clip: true clip: true
height: row2.height height: row2.height
width: 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 fillMode: Image.PreserveAspectCrop
} }
} }

View file

@ -29,16 +29,15 @@ CoverBackground {
property var mData: appWindow.itemData property var mData: appWindow.itemData
RemoteImage { RemoteImage {
anchors.fill: parent anchors.fill: parent
source: mData.ImageTags["Primary"] ? ApiClient.baseUrl + "/Items/" + mData.Id source: Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width})
+ "/Images/Primary?maxHeight=" + height + "&tag=" + mData.ImageTags["Primary"]
: ""
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
onSourceChanged: console.log(source)
} }
Shim { Shim {
// Movies usually show their name on the poster, // Movies usually show their name on the poster,
// so showing it here as well is a bit double // so showing it here as well is a bit double
visible: itemData.Type !== "Movie" visible: itemData.type !== "Movie"
anchors { anchors {
left: parent.left left: parent.left
right: parent.right right: parent.right
@ -52,7 +51,7 @@ CoverBackground {
top: parent.top top: parent.top
left: parent.left left: parent.left
} }
width: itemData.UserData.PlayedPercentage / 100 * parent.width width: itemData.userData.playedPercentage / 100 * parent.width
height: Theme.paddingSmall height: Theme.paddingSmall
color: Theme.highlightColor color: Theme.highlightColor
} }
@ -72,13 +71,13 @@ CoverBackground {
right: parent.right right: parent.right
} }
color: Theme.primaryColor color: Theme.primaryColor
text: itemData.Name text: itemData.name
truncationMode: TruncationMode.Fade truncationMode: TruncationMode.Fade
} }
Label { Label {
visible: typeof itemData.RunTimeTicks !== "undefined" visible: typeof itemData.runTimeTicks !== "undefined"
color: Theme.secondaryColor color: Theme.secondaryColor
text: Utils.ticksToText(itemData.RunTimeTicks) text: Utils.ticksToText(itemData.runTimeTicks)
} }
} }
} }

View file

@ -25,30 +25,22 @@ import nl.netsoj.chris.Jellyfin 1.0
import "../components" import "../components"
CoverBackground { PosterCover {
readonly property MediaPlayer player: appWindow.mediaPlayer readonly property MediaPlayer player: appWindow.mediaPlayer
property var mData: appWindow.itemData property var mData: appWindow.itemData
Rectangle { // Wanted to display the currently running move on here, but it's hard :/
/*Rectangle {
anchors.fill: parent anchors.fill: parent
color: "black" color: "black"
// Wanted to display the currently running move on here, but it's hard :/ VideoOutput {
/*VideoOutput {
id: coverOutput id: coverOutput
anchors.fill: parent anchors.fill: parent
source: player 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 { Shim {
anchors { anchors {

View file

@ -83,7 +83,7 @@ Page {
//appWindow.itemData = ({}) //appWindow.itemData = ({})
} }
if (status == PageStatus.Active) { if (status == PageStatus.Active) {
appWindow.itemData = jItem
} }
} }
} }

View file

@ -43,7 +43,7 @@ BaseDetailPage {
cellHeight: Utils.usePortraitCover(itemData.CollectionType) ? Constants.libraryDelegatePosterHeight cellHeight: Utils.usePortraitCover(itemData.CollectionType) ? Constants.libraryDelegatePosterHeight
: Constants.libraryDelegateHeight : Constants.libraryDelegateHeight
header: PageHeader { header: PageHeader {
title: itemData.Name || qsTr("Loading") title: itemData.name || qsTr("Loading")
} }
PullDownMenu { PullDownMenu {
id: downMenu id: downMenu
@ -58,7 +58,7 @@ BaseDetailPage {
RemoteImage { RemoteImage {
id: itemImage id: itemImage
anchors.fill: parent 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) fallbackColor: Utils.colorFromString(model.name)
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
clip: true clip: true

View file

@ -68,7 +68,7 @@ BaseDetailPage {
shimColor: Theme.overlayBackgroundColor shimColor: Theme.overlayBackgroundColor
shimOpacity: Theme.opacityOverlay shimOpacity: Theme.opacityOverlay
//width: model.userData.PlayedPercentage * parent.width / 100 //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 { Rectangle {
@ -78,9 +78,28 @@ BaseDetailPage {
bottom: parent.bottom bottom: parent.bottom
} }
height: Theme.paddingMedium height: Theme.paddingMedium
width: model.userData.playedPercentage * parent.width / 100 width: model.userData.playedPercentage * parent.width / 100
color: Theme.highlightColor 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 { Label {
@ -129,9 +148,9 @@ BaseDetailPage {
} }
onStatusChanged: { onStatusChanged: {
if (status == PageStatus.Active) { if (status == PageStatus.Active) {
console.log(JSON.stringify(itemData)) //console.log(JSON.stringify(itemData))
episodeModel.show = itemData.seriesId episodeModel.show = itemData.seriesId
episodeModel.seasonId = itemData.jellyfinId episodeModel.seasonId = itemData.jellyfinId
} }
} }
} }