From 228f81984bf0a0f303336b31430a8ef4102dfae5 Mon Sep 17 00:00:00 2001 From: Chris Josten Date: Mon, 29 Mar 2021 23:48:16 +0200 Subject: [PATCH] WIP: Slowly bringing back viewmodels --- core/include/JellyfinQt/apimodel.h | 6 +- core/include/JellyfinQt/jellyfin.h | 1 + core/include/JellyfinQt/model/item.h | 10 ++ core/include/JellyfinQt/support/jsonconv.h | 17 +++- core/include/JellyfinQt/viewmodel/item.h | 6 +- core/include/JellyfinQt/viewmodel/itemmodel.h | 93 ++++++++++++++++--- core/include/JellyfinQt/viewmodel/loader.h | 19 ++-- .../JellyfinQt/viewmodel/playbackmanager.h | 6 +- core/src/apimodel.cpp | 27 +++++- core/src/jellyfin.cpp | 19 ++-- core/src/model/item.cpp | 4 + core/src/support/jsonconv.cpp | 44 +++++++-- core/src/viewmodel/item.cpp | 14 +-- core/src/viewmodel/itemmodel.cpp | 39 ++------ qtquick/qml/main.qml | 21 +++++ qtquick/qml/pages/DetailPage.qml | 30 +++++- qtquick/qml/pages/MainPage.qml | 32 ++++--- 17 files changed, 292 insertions(+), 96 deletions(-) diff --git a/core/include/JellyfinQt/apimodel.h b/core/include/JellyfinQt/apimodel.h index 5fef3cd..352ed31 100644 --- a/core/include/JellyfinQt/apimodel.h +++ b/core/include/JellyfinQt/apimodel.h @@ -168,7 +168,7 @@ public: m_startIndex = 0; m_totalRecordCount = -1; emitModelShouldClear(); - loadMore(0, -1, ViewModel::ModelStatus::Loading); + loadMore(0, m_limit, ViewModel::ModelStatus::Loading); } void loadMore() { @@ -272,7 +272,9 @@ protected: // meaning loadMore is not supported. return; } - setRequestLimit

(this->m_parameters, limit); + if (limit > 0) { + setRequestLimit

(this->m_parameters, limit); + } this->setStatus(suggestedModelStatus); // We never want to set this while the loader is running, hence the Mutex and setting it here diff --git a/core/include/JellyfinQt/jellyfin.h b/core/include/JellyfinQt/jellyfin.h index 2a2ddfa..5bc28d0 100644 --- a/core/include/JellyfinQt/jellyfin.h +++ b/core/include/JellyfinQt/jellyfin.h @@ -30,6 +30,7 @@ #include "apiclient.h" #include "apimodel.h" #include "serverdiscoverymodel.h" +#include "websocket.h" #include "viewmodel/item.h" #include "viewmodel/itemmodel.h" #include "viewmodel/loader.h" diff --git a/core/include/JellyfinQt/model/item.h b/core/include/JellyfinQt/model/item.h index e313f74..9675a58 100644 --- a/core/include/JellyfinQt/model/item.h +++ b/core/include/JellyfinQt/model/item.h @@ -33,6 +33,16 @@ namespace Model { class Item : public DTO::BaseItemDto { public: + /** + * @brief Constructor that creates an empty item. + */ + Item(); + + /** + * @brief Copies the data from the DTO into this model and attaches an ApiClient + * @param data The DTO to copy information from + * @param apiClient The ApiClient to attach to, to listen for updates and so on. + */ Item(const DTO::BaseItemDto &data, ApiClient *apiClient = nullptr); virtual ~Item(); diff --git a/core/include/JellyfinQt/support/jsonconv.h b/core/include/JellyfinQt/support/jsonconv.h index 1d78231..c8cc6c9 100644 --- a/core/include/JellyfinQt/support/jsonconv.h +++ b/core/include/JellyfinQt/support/jsonconv.h @@ -152,9 +152,24 @@ QJsonValue toJsonValue(const QSharedPointer &source, convertType +QString toString(const T &source, convertType) { + return toJsonValue(source).toString(); +} + +template +QString toString(const std::optional &source, convertType>) { + if (source.has_value()) { + return toString(source.value(), convertType{}); + } else { + return QString(); + } +} + template QString toString(const T &source) { - return toJsonValue(source).toString(); + return toString(source, convertType{}); } diff --git a/core/include/JellyfinQt/viewmodel/item.h b/core/include/JellyfinQt/viewmodel/item.h index f3ef7b8..74d5eae 100644 --- a/core/include/JellyfinQt/viewmodel/item.h +++ b/core/include/JellyfinQt/viewmodel/item.h @@ -51,9 +51,7 @@ namespace ViewModel { class Item : public QObject { Q_OBJECT public: - explicit Item(QObject *parent = nullptr); - explicit Item(QSharedPointer data = QSharedPointer(), - QObject *parent = nullptr); + explicit Item(QObject *parent = nullptr, QSharedPointer data = QSharedPointer::create()); // Please keep the order of the properties the same as in the file linked above. Q_PROPERTY(QUuid jellyfinId READ jellyfinId NOTIFY jellyfinIdChanged) @@ -194,7 +192,7 @@ public: Q_PROPERTY(QString itemId READ itemId WRITE setItemId NOTIFY itemIdChanged) QString itemId() const { return m_parameters.itemId(); } - void setItemId(QString newItemId) { m_parameters.setItemId(newItemId); } + void setItemId(QString newItemId) { m_parameters.setItemId(newItemId); emit itemIdChanged(newItemId); } virtual bool canReload() const override; signals: diff --git a/core/include/JellyfinQt/viewmodel/itemmodel.h b/core/include/JellyfinQt/viewmodel/itemmodel.h index 8516ef2..03c8557 100644 --- a/core/include/JellyfinQt/viewmodel/itemmodel.h +++ b/core/include/JellyfinQt/viewmodel/itemmodel.h @@ -44,7 +44,44 @@ namespace ViewModel { // This file contains all models that expose a Model::Item -using UserViewsLoaderBase = LoaderModelLoader; +/** + * @brief Class intended for models which have a mandatory userId property, which can be extracted from the + * ApiClient. + */ +template +class AbstractUserParameterLoader : public LoaderModelLoader { +public: + explicit AbstractUserParameterLoader(Support::Loader *loader, QObject *parent = nullptr) + : LoaderModelLoader(loader, parent) { + this->connect(this, &BaseModelLoader::apiClientChanged, this, &AbstractUserParameterLoader::apiClientChanged); + } +protected: + virtual bool canReload() const override { + return BaseModelLoader::canReload() && !this->m_parameters.userId().isNull(); + } +private: + void apiClientChanged(ApiClient *newApiClient) { + if (this->m_apiClient != nullptr) { + this->disconnect(this->m_apiClient, &ApiClient::userIdChanged, this, &AbstractUserParameterLoader::userIdChanged); + } + if (newApiClient != nullptr) { + this->connect(newApiClient, &ApiClient::userIdChanged, this, &AbstractUserParameterLoader::userIdChanged); + if (!newApiClient->userId().isNull()) { + this->m_parameters.setUserId(newApiClient->userId()); + } + } + } + + void userIdChanged(const QString &newUserId) { + this->m_parameters.setUserId(newUserId); + this->autoReloadIfNeeded(); + } +}; + +/** + * Loads the views of an user, such as "Videos", "Music" and so on. + */ +using UserViewsLoaderBase = AbstractUserParameterLoader; class UserViewsLoader : public UserViewsLoaderBase { Q_OBJECT public: @@ -53,12 +90,27 @@ public: FWDPROP(bool, includeExternalContent, IncludeExternalContent) FWDPROP(bool, includeHidden, IncludeHidden) FWDPROP(QStringList, presetViews, PresetViews) -private slots: - void apiClientChanged(ApiClient *newApiClient); - void userIdChanged(const QString &newUserId); }; -using UserItemsLoaderBase = LoaderModelLoader; +using LatestMediaBase = AbstractUserParameterLoader, Jellyfin::Loader::GetLatestMediaParams>; +class LatestMediaLoader : public LatestMediaBase { + Q_OBJECT +public: + explicit LatestMediaLoader(QObject *parent = nullptr); + + // Optional + FWDPROP(QList, enableImageTypes, EnableImageTypes) + FWDPROP(bool, enableImages, EnableImages) + FWDPROP(bool, enableUserData, EnableUserData) + FWDPROP(QList, fields, Fields) + FWDPROP(bool, groupItems, GroupItems) + FWDPROP(qint32, imageTypeLimit, ImageTypeLimit) + FWDPROP(QStringList, includeItemTypes, IncludeItemTypes) + FWDPROP(bool, isPlayed, IsPlayed) + FWDPROP(QString, parentId, ParentId) +}; + +using UserItemsLoaderBase = AbstractUserParameterLoader; class UserItemsLoader : public UserItemsLoaderBase { Q_OBJECT public: @@ -70,14 +122,26 @@ public: FWDPROP(QStringList, albums, Albums) FWDPROP(QStringList, artistIds, ArtistIds) FWDPROP(QStringList, artists, Artists) + FWDPROP(bool, collapseBoxSetItems, CollapseBoxSetItems) + FWDPROP(QStringList, contributingArtistIds, ContributingArtistIds) + FWDPROP(QList, enableImageTypes, EnableImageTypes); + FWDPROP(bool, enableImages, EnableImages) + FWDPROP(bool, enableTotalRecordCount, EnableTotalRecordCount) + FWDPROP(bool, enableUserData, EnableUserData) + FWDPROP(QStringList, excludeArtistIds, ExcludeArtistIds) + FWDPROP(QStringList, excludeItemIds, ExcludeItemIds) + FWDPROP(QStringList, excludeItemTypes, ExcludeItemTypes) + FWDPROP(QList, excludeLocationTypes, ExcludeLocationTypes) + FWDPROP(QList, fields, Fields) + FWDPROP(QList, filters, Filters) + + FWDPROP(QString, parentId, ParentId) FWDPROP(bool, recursive, Recursive) //FWDPROP(bool, collapseBoxSetItems) -protected: - virtual bool canReload() const override; -private slots: - void apiClientChanged(ApiClient *newApiClient); - void userIdChanged(const QString &newUserId); }; + + + /** * @brief Base class for each model that works with items. */ @@ -95,7 +159,10 @@ public: playlistItemId, dateCreated, dateLastMediaAdded, - extraType + extraType, + + // Hand-picked, important ones + imageTags }; explicit ItemModel (QObject *parent = nullptr); @@ -111,7 +178,9 @@ public: JFRN(playlistItemId), JFRN(dateCreated), JFRN(dateLastMediaAdded), - JFRN(extraType) + JFRN(extraType), + // Handpicked, important ones + JFRN(imageTags), }; } QVariant data(const QModelIndex &index, int role) const override; diff --git a/core/include/JellyfinQt/viewmodel/loader.h b/core/include/JellyfinQt/viewmodel/loader.h index f065afa..3ed7f9d 100644 --- a/core/include/JellyfinQt/viewmodel/loader.h +++ b/core/include/JellyfinQt/viewmodel/loader.h @@ -144,13 +144,14 @@ template class Loader : public LoaderBase { using RFutureWatcher = QFutureWatcher>; public: - Loader(Support::Loader loaderImpl, QObject *parent = nullptr) + Loader(Support::Loader *loaderImpl, QObject *parent = nullptr) : Loader(nullptr, loaderImpl, parent) {} - Loader(ApiClient *apiClient, Support::Loader loaderImpl, QObject *parent = nullptr) + Loader(ApiClient *apiClient, Support::Loader *loaderImpl, QObject *parent = nullptr) : LoaderBase(apiClient, parent), m_loader(loaderImpl), - m_futureWatcher(new QFutureWatcher>) { + m_futureWatcher(new QFutureWatcher>(this)) { + m_dataViewModel = new T(this); connect(m_futureWatcher, &RFutureWatcher::finished, this, &Loader::updateData); } @@ -161,8 +162,9 @@ public: void reload() override { if (m_futureWatcher->isRunning()) return; setStatus(Loading); - m_loader.setParameters(m_parameters); - m_loader.prepareLoad(); + this->m_loader->setApiClient(m_apiClient); + m_loader->setParameters(m_parameters); + m_loader->prepareLoad(); QFuture> future = QtConcurrent::run(this, &Loader::invokeLoader); m_futureWatcher->setFuture(future); } @@ -172,7 +174,7 @@ protected: /** * @brief Subclasses should initialize this to a loader that actually loads stuff. */ - Support::Loader m_loader; + QScopedPointer> m_loader = nullptr; private: QFutureWatcher> *m_futureWatcher; @@ -184,9 +186,8 @@ private: */ std::optional invokeLoader() { QMutexLocker(&this->m_mutex); - this->m_loader.setApiClient(m_apiClient); try { - return this->m_loader.load(); + return this->m_loader->load(); } catch (Support::LoadException &e) { qWarning() << "Exception while loading an item: " << e.what(); this->setErrorString(QString(e.what())); @@ -206,7 +207,7 @@ private: } else { // Replace the model using PointerType = typename decltype(m_dataViewModel->data())::Type; - m_dataViewModel = new T(QSharedPointer::create(newData), this); + m_dataViewModel = new T(this, QSharedPointer::create(newData, m_apiClient)); } setStatus(Ready); emitDataChanged(); diff --git a/core/include/JellyfinQt/viewmodel/playbackmanager.h b/core/include/JellyfinQt/viewmodel/playbackmanager.h index 8743d20..0859fab 100644 --- a/core/include/JellyfinQt/viewmodel/playbackmanager.h +++ b/core/include/JellyfinQt/viewmodel/playbackmanager.h @@ -80,7 +80,7 @@ public: Q_PROPERTY(PlayMethod playMethod READ playMethod NOTIFY playMethodChanged) // Current Item and queue informatoion - Q_PROPERTY(Model::Item *item READ item NOTIFY itemChanged) + Q_PROPERTY(ViewModel::Item *item READ item NOTIFY itemChanged) Q_PROPERTY(QAbstractItemModel *queue READ queue NOTIFY queueChanged) Q_PROPERTY(int queueIndex READ queueIndex NOTIFY queueIndexChanged) @@ -94,7 +94,7 @@ public: Q_PROPERTY(QMediaPlayer::State playbackState READ playbackState NOTIFY playbackStateChanged) Q_PROPERTY(qint64 position READ position NOTIFY positionChanged) - Model::Item *item() const { return m_item.data(); } + ViewModel::Item *item() const { return m_displayItem.get(); } void setApiClient(ApiClient *apiClient); QString streamUrl() const { return m_streamUrl; } @@ -169,6 +169,8 @@ private: QTimer m_updateTimer; ApiClient *m_apiClient = nullptr; QSharedPointer m_item; + QScopedPointer m_displayItem = QScopedPointer(new ViewModel::Item()); + QString m_streamUrl; QString m_playSessionId; int m_audioIndex = 0; diff --git a/core/src/apimodel.cpp b/core/src/apimodel.cpp index 90d8186..90e0839 100644 --- a/core/src/apimodel.cpp +++ b/core/src/apimodel.cpp @@ -62,7 +62,7 @@ void BaseModelLoader::setApiClient(ApiClient *newApiClient) { void BaseModelLoader::setLimit(int newLimit) { int oldLimit = this->m_limit; m_limit = newLimit; - if (oldLimit != this->m_limit) { + if (oldLimit != newLimit) { emit limitChanged(this->m_limit); } } @@ -76,6 +76,7 @@ void BaseModelLoader::setAutoReload(bool newAutoReload) { bool BaseModelLoader::canReload() const { return m_apiClient != nullptr + && !m_isBeingParsed // If the loader for this model needs authentication (almost every one does) // block if the ApiClient is not authenticated yet. && (!m_needsAuthentication || m_apiClient->authenticated()) @@ -88,6 +89,8 @@ void BaseApiModel::reload() { qWarning() << " BaseApiModel slot called instead of overloaded method"; } +// Parameters injectors and result extractors + template <> bool setRequestStartIndex(Loader::GetUserViewsParams ¶ms, int startIndex) { // Not supported @@ -112,6 +115,28 @@ int extractTotalRecordCount(const DTO::BaseItemDtoQueryResult &result) { return result.totalRecordCount(); } +template <> +QList extractRecords(const QList &result) { + return result; +} + +template <> +int extractTotalRecordCount(const QList &result) { + return result.size(); +} + +template<> +void setRequestLimit(Loader::GetLatestMediaParams ¶ms, int limit) { + params.setLimit(limit); +} + +template<> +bool setRequestStartIndex(Loader::GetLatestMediaParams ¶ms, int offset) { + Q_UNUSED(params) + Q_UNUSED(offset) + return false; +} + void registerModels(const char *URI) { Q_UNUSED(URI) diff --git a/core/src/jellyfin.cpp b/core/src/jellyfin.cpp index d6ec414..d4a2630 100644 --- a/core/src/jellyfin.cpp +++ b/core/src/jellyfin.cpp @@ -22,20 +22,25 @@ namespace Jellyfin { void registerTypes(const char *uri) { qmlRegisterType(uri, 1, 0, "ApiClient"); qmlRegisterType(uri, 1, 0, "ServerDiscoveryModel"); + qmlRegisterType(uri, 1, 0, "PlaybackManager"); + qmlRegisterUncreatableType(uri, 1, 0, "Item", "Acquire one via ItemLoader or exposed properties"); + qmlRegisterUncreatableType(uri, 1, 0, "WebSocket", "Obtain one via your ApiClient"); - + // AbstractItemModels qmlRegisterUncreatableType(uri, 1, 0, "BaseApiModel", "Please use one of its subclasses"); qmlRegisterUncreatableType(uri, 1, 0, "BaseModelLoader", "Please use one of its subclasses"); - qmlRegisterUncreatableType(uri, 1, 0, "LoaderBase", "Use on eof its subclasses"); - - qmlRegisterUncreatableType(uri, 1, 0, "Item", "Acquire one via ItemLoader or exposed properties"); - qmlRegisterType(uri, 1, 0, "ItemLoader"); qmlRegisterType(uri, 1, 0, "ItemModel"); - qmlRegisterType(uri, 1, 0, "UsersViewLoader"); - qmlRegisterType(uri, 1, 0, "PlaybackManager"); + // Loaders + qmlRegisterUncreatableType(uri, 1, 0, "LoaderBase", "Use one of its subclasses"); + qmlRegisterType(uri, 1, 0, "ItemLoader"); + qmlRegisterType(uri, 1, 0, "LatestMediaLoader"); + qmlRegisterType(uri, 1, 0, "UserItemsLoader"); + qmlRegisterType(uri, 1, 0, "UsersViewsLoader"); + // Enumerations qmlRegisterUncreatableType(uri, 1, 0, "GeneralCommandType", "Is an enum"); qmlRegisterUncreatableType(uri, 1, 0, "ModelStatus", "Is an enum"); + } } diff --git a/core/src/model/item.cpp b/core/src/model/item.cpp index f0e6583..f2de2e5 100644 --- a/core/src/model/item.cpp +++ b/core/src/model/item.cpp @@ -22,6 +22,10 @@ namespace Jellyfin { namespace Model { + +Item::Item() + : Item(DTO::BaseItemDto(), nullptr) { } + Item::Item(const DTO::BaseItemDto &data, ApiClient *apiClient) : DTO::BaseItemDto(data), m_apiClient(apiClient) { if (m_apiClient != nullptr) { diff --git a/core/src/support/jsonconv.cpp b/core/src/support/jsonconv.cpp index 5889c8d..f6a959e 100644 --- a/core/src/support/jsonconv.cpp +++ b/core/src/support/jsonconv.cpp @@ -149,8 +149,14 @@ QJsonValue toJsonValue(const QStringList &source, convertType QJsonObject fromJsonValue(const QJsonValue &source, convertType) { - if (!source.isObject()) throw ParseException("Error parsing JSON value as object: not a double"); - return source.toObject(); + switch(source.type()) { + case QJsonValue::Null: + return QJsonObject(); + case QJsonValue::Object: + return source.toObject(); + default: + throw ParseException("Error parsing JSON value as object: not an object"); + } } template <> @@ -161,7 +167,7 @@ QJsonValue toJsonValue(const QJsonObject &source, convertType double fromJsonValue(const QJsonValue &source, convertType) { - if (!source.isDouble()) throw ParseException("Error parsing JSON value as integer: not a double"); + if (!source.isDouble()) throw ParseException("Error parsing JSON value as double: not a double"); return source.toDouble(); } @@ -170,6 +176,18 @@ QJsonValue toJsonValue(const double &source, convertType) { return QJsonValue(source); } +// Float +template <> +float fromJsonValue(const QJsonValue &source, convertType) { + if (!source.isDouble()) throw ParseException("Error parsing JSON value as float: not a double"); + return static_cast(source.toDouble()); +} + +template <> +QJsonValue toJsonValue(const float &source, convertType) { + return QJsonValue(static_cast(source)); +} + // QDateTime template <> QDateTime fromJsonValue(const QJsonValue &source, convertType) { @@ -205,27 +223,37 @@ QJsonValue toJsonValue(const QUuid &source, convertType) { // String types template <> -QString toString(const QUuid &source) { +QString toString(const QUuid &source, convertType) { return uuidToString(source); } template <> -QString toString(const qint32 &source) { +QString toString(const qint32 &source, convertType) { return QString::number(source); } template <> -QString toString(const qint64 &source) { +QString toString(const qint64 &source, convertType) { return QString::number(source); } template <> -QString toString(const bool &source) { +QString toString(const float &source, convertType) { + return QString::number(source); +} + +template <> +QString toString(const double &source, convertType) { + return QString::number(source); +} + +template <> +QString toString(const bool &source, convertType) { return source ? QStringLiteral("true") : QStringLiteral("false"); } template <> -QString toString(const QString &source) { +QString toString(const QString &source, convertType) { return source; } diff --git a/core/src/viewmodel/item.cpp b/core/src/viewmodel/item.cpp index 8da4aa4..b7a673c 100644 --- a/core/src/viewmodel/item.cpp +++ b/core/src/viewmodel/item.cpp @@ -21,11 +21,10 @@ namespace Jellyfin { namespace ViewModel { -Item::Item(QObject *parent) - : Item(nullptr, parent){} +Item::Item(QObject *parent, QSharedPointer data) + : QObject(parent), m_data(data){ -Item::Item(QSharedPointer data, QObject *parent) - : QObject(parent), m_data(data){} +} void Item::setData(QSharedPointer newData) { Model::Item oldData = *m_data.data(); @@ -36,7 +35,8 @@ void Item::setData(QSharedPointer newData) { // ItemLoader ItemLoader::ItemLoader(QObject *parent) - : BaseClass(Jellyfin::Loader::HTTP::GetItemLoader(), parent) { + : BaseClass(new Jellyfin::Loader::HTTP::GetItemLoader(), parent) { + connect(this, &LoaderBase::apiClientChanged, this, &ItemLoader::onApiClientChanged); } void ItemLoader::onApiClientChanged(ApiClient *newApiClient) { @@ -54,7 +54,9 @@ void ItemLoader::setUserId(const QString &newUserId) { } bool ItemLoader::canReload() const { - return BaseClass::canReload() && !m_parameters.itemId().isEmpty(); + return BaseClass::canReload() + && !m_parameters.itemId().isEmpty() + && !m_parameters.userId().isEmpty(); } } diff --git a/core/src/viewmodel/itemmodel.cpp b/core/src/viewmodel/itemmodel.cpp index 7a5c84c..36d0ba1 100644 --- a/core/src/viewmodel/itemmodel.cpp +++ b/core/src/viewmodel/itemmodel.cpp @@ -18,6 +18,9 @@ */ #include "JellyfinQt/viewmodel/itemmodel.h" +#include "JellyfinQt/loader/http/getlatestmedia.h" +#include "JellyfinQt/loader/http/getitemsbyuserid.h" + #define JF_CASE(roleName) case roleName: \ try { \ return QVariant(item.roleName()); \ @@ -30,37 +33,13 @@ namespace Jellyfin { namespace ViewModel { UserViewsLoader::UserViewsLoader(QObject *parent) - : UserViewsLoaderBase(new Jellyfin::Loader::HTTP::GetUserViewsLoader(), parent) { - connect(this, &BaseModelLoader::apiClientChanged, this, &UserViewsLoader::apiClientChanged); -} + : UserViewsLoaderBase(new Jellyfin::Loader::HTTP::GetUserViewsLoader(), parent) { } -void UserViewsLoader::apiClientChanged(ApiClient *newApiClient) { - if (m_apiClient != nullptr) disconnect(m_apiClient, &ApiClient::userIdChanged, this, &UserViewsLoader::userIdChanged); - if (newApiClient != nullptr) { - connect(newApiClient, &ApiClient::userIdChanged, this, &UserViewsLoader::userIdChanged); - if (!newApiClient->userId().isNull()) { - m_parameters.setUserId(newApiClient->userId()); - } - } -} +LatestMediaLoader::LatestMediaLoader(QObject *parent) + : LatestMediaBase(new Jellyfin::Loader::HTTP::GetLatestMediaLoader(), parent){ } -void UserViewsLoader::userIdChanged(const QString &newUserId) { - m_parameters.setUserId(newUserId); - autoReloadIfNeeded(); -} -void UserItemsLoader::apiClientChanged(ApiClient *newApiClient) { - if (m_apiClient != nullptr) disconnect(m_apiClient, &ApiClient::userIdChanged, this, &UserItemsLoader::userIdChanged); - if (newApiClient != nullptr) connect(newApiClient, &ApiClient::userIdChanged, this, &UserItemsLoader::userIdChanged); -} - -void UserItemsLoader::userIdChanged(const QString &newUserId) { - m_parameters.setUserId(newUserId); - autoReloadIfNeeded(); -} - -bool UserItemsLoader::canReload() const { - return BaseModelLoader::canReload() && !m_parameters.userId().isNull(); -} +UserItemsLoader::UserItemsLoader(QObject *parent) + : UserItemsLoaderBase(new Jellyfin::Loader::HTTP::GetItemsByUserIdLoader(), parent) {} ItemModel::ItemModel(QObject *parent) : ApiModel(parent) { } @@ -81,6 +60,8 @@ QVariant ItemModel::data(const QModelIndex &index, int role) const { JF_CASE(dateCreated) JF_CASE(dateLastMediaAdded) JF_CASE(extraType) + // Handpicked, important ones + JF_CASE(imageTags) default: return QVariant(); } diff --git a/qtquick/qml/main.qml b/qtquick/qml/main.qml index 65d9116..88ee587 100644 --- a/qtquick/qml/main.qml +++ b/qtquick/qml/main.qml @@ -14,6 +14,11 @@ ApplicationWindow { height: 600 visible: true property int _oldDepth: 0 + property alias playbackManager: playbackManager + + J.PlaybackManager { + id: playbackManager + } background: Background { id: background @@ -43,4 +48,20 @@ ApplicationWindow { Component.onCompleted: { ApiClient.restoreSavedSession() } + + footer: Column { + id: footer + Text { + text: qsTr("Now playing") + color: "white" + } + Text { + text: playbackManager.item.name ? playbackManager.item.name : "Nothing" + color: "white" + } + } + Rectangle { + color: "darkblue" + anchors.fill: footer + } } diff --git a/qtquick/qml/pages/DetailPage.qml b/qtquick/qml/pages/DetailPage.qml index 91d2994..b7dc2ba 100644 --- a/qtquick/qml/pages/DetailPage.qml +++ b/qtquick/qml/pages/DetailPage.qml @@ -6,12 +6,14 @@ import nl.netsoj.chris.Jellyfin 1.0 as J import "../components" import "../.." +import ".." Page { + id: detailPage property bool _modelsLoaded: false property StackView stackView: StackView.view property string itemId - property alias jellyfinItem: jellyfinItem.data + property alias jellyfinItem: jellyfinItemLoader.data header: ToolBar { Label { anchors.horizontalCenter: parent.horizontalCenter @@ -25,13 +27,33 @@ Page { onClicked: stackView.pop() } J.ItemLoader { - id: jellyfinItem - jellyfinId: itemId + id: jellyfinItemLoader + itemId: detailPage.itemId apiClient: ApiClient } Image { - anchors.centerIn: parent + anchors.top: parent.top + width: parent.width + height: parent.height / 3 source: ApiClient.baseUrl + "/Items/" + itemId + "/Images/Primary?tag=" + jellyfinItem.tag } + + ListView { + width: parent.width + height: parent.height / 3 * 2 + anchors.bottom: parent.bottom + model: J.ItemModel { + loader: J.UserItemsLoader { + apiClient: ApiClient + parentId: detailPage.itemId + } + } + delegate: ItemDelegate{ + icon.source: ApiClient.baseUrl + "/Items/" + model.jellyfinId + "/Images/Primary?tag=" + model.tag + text: model.name + width: parent.width + onClicked: playbackManager.play(model.jellyfinId) + } + } } diff --git a/qtquick/qml/pages/MainPage.qml b/qtquick/qml/pages/MainPage.qml index c5b1ba8..2bff4eb 100644 --- a/qtquick/qml/pages/MainPage.qml +++ b/qtquick/qml/pages/MainPage.qml @@ -21,7 +21,7 @@ Page { J.ItemModel { id: mediaLibraryModel - loader: J.UsersViewLoader { + loader: J.UsersViewsLoader { id: mediaLibraryModelLoader apiClient: ApiClient } @@ -30,6 +30,7 @@ Page { ScrollView { anchors.fill: parent contentHeight: content.height + contentWidth: availableWidth Column { id: content width: parent.width @@ -37,12 +38,15 @@ Page { model: mediaLibraryModel Column { width: parent.width - /*J.UserItemLatestModel { + J.ItemModel{ id: userItemModel - apiClient: ApiClient - parentId: model.id - limit: 16 - }*/ + loader: J.LatestMediaLoader { + id: latestMediaLoader + apiClient: ApiClient + parentId: model.jellyfinId + //limit: 16 + } + } Label { text: model.name ? model.name : "" } @@ -51,13 +55,14 @@ Page { width: parent.width height: SailfinStyle.unit * 20 orientation: ListView.Horizontal - model: 10 // userItemModel + model: userItemModel delegate: ItemDelegate { width: 12 * SailfinStyle.unit height: 20 * SailfinStyle.unit Image { anchors.fill: parent - source: ApiClient.baseUrl + "/Items/" + model.id + "/Images/Primary?tag=" + model.tag + source: ApiClient.baseUrl + "/Items/" + model.jellyfinId + + "/Images/Primary?tag=" + model.imageTags["Primary"] //model.tag } Label { anchors.left: parent.left @@ -65,14 +70,18 @@ Page { anchors.right: parent.right text: model.name } - onClicked: stackView.push(Qt.resolvedUrl("DetailPage.qml"), {"itemId": model.id}) + onClicked: stackView.push(Qt.resolvedUrl( + "DetailPage.qml"), { + "itemId": model.jellyfinId + }) } } Connections { target: mediaLibraryModelLoader onReady: { if (mediaLibraryModelLoader.status === ModelStatus.Ready) { - //userItemModel.reload() + + latestMediaLoader.reload() } } } @@ -81,13 +90,14 @@ Page { } } + /** * Loads models if not laoded. Set force to true to reload models * even if loaded. */ function loadModels(force) { if (force || (ApiClient.authenticated && !_modelsLoaded)) { - _modelsLoaded = true; + _modelsLoaded = true mediaLibraryModel.reload() } }