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 {