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

Ready model for infinite lists

This commit is contained in:
Chris Josten 2020-09-27 03:14:05 +02:00
parent 1ba7f6f8ef
commit 5ea17070fe
9 changed files with 151 additions and 37 deletions

View file

@ -39,6 +39,7 @@ DISTFILES += \
qml/components/RemoteImage.qml \ qml/components/RemoteImage.qml \
qml/components/UserGridDelegate.qml \ qml/components/UserGridDelegate.qml \
qml/components/VideoPlayer.qml \ qml/components/VideoPlayer.qml \
qml/components/itemdetails/CollectionFolder.qml \
qml/components/itemdetails/EpisodeDetails.qml \ qml/components/itemdetails/EpisodeDetails.qml \
qml/components/itemdetails/FilmDetails.qml \ qml/components/itemdetails/FilmDetails.qml \
qml/components/itemdetails/PlayToolbar.qml \ qml/components/itemdetails/PlayToolbar.qml \

View file

@ -0,0 +1,5 @@
import QtQuick 2.0
Item {
}

View file

@ -61,7 +61,6 @@ CoverBackground {
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
Component.onCompleted: console.log(JSON.stringify(model.imageTags))
} }
} }
@ -105,7 +104,6 @@ CoverBackground {
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
Component.onCompleted: console.log(JSON.stringify(model.imageTags))
} }
} }

View file

@ -47,14 +47,14 @@ Page {
apiClient: ApiClient apiClient: ApiClient
} }
MoreSection { MoreSection {
text: qsTr("Resume watching") text: qsTr("Resume watching")
clickable: false clickable: false
} }
MoreSection { MoreSection {
text: qsTr("Next up") text: qsTr("Next up")
clickable: false clickable: false
} }
UserViewModel { UserViewModel {
id: mediaLibraryModel id: mediaLibraryModel
@ -95,7 +95,7 @@ Page {
property string id: model.id property string id: model.id
title: model.name 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 /*model.imageTags["Primary"] ? ApiClient.baseUrl + "/Items/" + model.id
+ "/Images/Primary?maxHeight=" + height + "&tag=" + model.imageTags["Primary"] + "/Images/Primary?maxHeight=" + height + "&tag=" + model.imageTags["Primary"]
: ""*/ : ""*/
landscape: !Utils.usePortraitCover(model.type) landscape: !Utils.usePortraitCover(model.type)
@ -113,6 +113,7 @@ Page {
Connections { Connections {
target: mediaLibraryModel target: mediaLibraryModel
onStatusChanged: { onStatusChanged: {
console.log("MediaLibraryModel status " + status)
if (status == ApiModel.Ready) { if (status == ApiModel.Ready) {
userItemModel.reload() userItemModel.reload()
} }
@ -165,5 +166,5 @@ Page {
_modelsLoaded = true; _modelsLoaded = true;
mediaLibraryModel.reload() mediaLibraryModel.reload()
} }
} }
} }

View file

@ -1,6 +1,6 @@
#include "credentialmanager.h" #include "credentialmanager.h"
CredentialsManager * CredentialsManager::getInstance(QObject *parent) { CredentialsManager * CredentialsManager::newInstance(QObject *parent) {
return new FallbackCredentialsManager(parent); return new FallbackCredentialsManager(parent);
} }

View file

@ -7,6 +7,13 @@
#include <QSettings> #include <QSettings>
#include <QString> #include <QString>
/**
* @brief The CredentialsManager class stores credentials for users.
*
* You can get an instance using ::instance(), which may depend on the platform being
* used. Since the implementation may be asynchronous, the methods won't return anything,
* they emit a corresponding signal instead.
*/
class CredentialsManager : public QObject { class CredentialsManager : public QObject {
Q_OBJECT Q_OBJECT
public: public:
@ -61,9 +68,11 @@ public:
/** /**
* @brief Retrieves an implementation which can store this token. * @brief Retrieves an implementation which can store this token.
* @param The parent to set the implementations QObject parent to * @param The parent to set the implementations QObject parent to
*
* This method is always guaranteed to return an instance.
* @return An implementation of this interface (may vary acrros platform). * @return An implementation of this interface (may vary acrros platform).
*/ */
static CredentialsManager *getInstance(QObject *parent = nullptr); static CredentialsManager *newInstance(QObject *parent = nullptr);
/** /**
* @return if the implementation of this interface stores the token in a secure place. * @return if the implementation of this interface stores the token in a secure place.

View file

@ -5,7 +5,7 @@ ApiClient::ApiClient(QObject *parent)
: QObject(parent) { : QObject(parent) {
m_deviceName = QHostInfo::localHostName(); m_deviceName = QHostInfo::localHostName();
m_deviceId = QUuid::createUuid().toString(); // TODO: make this not random? m_deviceId = QUuid::createUuid().toString(); // TODO: make this not random?
m_credManager = CredentialsManager::getInstance(this); m_credManager = CredentialsManager::newInstance(this);
generateDeviceProfile(); generateDeviceProfile();
} }

View file

@ -1,15 +1,27 @@
#include "jellyfinapimodel.h" #include "jellyfinapimodel.h"
namespace Jellyfin { namespace Jellyfin {
ApiModel::ApiModel(QString path, QString subfield, bool addUserId, QObject *parent) ApiModel::ApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent)
: QAbstractListModel (parent), : QAbstractListModel (parent),
m_path(path), m_path(path),
m_subfield(subfield), m_hasRecordResponse(hasRecordResponse),
m_addUserId(addUserId){ m_addUserId(addUserId){
} }
void ApiModel::reload() { void ApiModel::reload() {
this->setStatus(Loading); load(RELOAD);
}
void ApiModel::load(LoadType type) {
qDebug() << (type == RELOAD ? "RELOAD" : "LOAD_MORE");
switch(type) {
case RELOAD:
this->setStatus(Loading);
break;
case LOAD_MORE:
this->setStatus(LoadingMore);
break;
}
if (m_apiClient == nullptr) { if (m_apiClient == nullptr) {
qWarning() << "Please set the apiClient property before (re)loading"; qWarning() << "Please set the apiClient property before (re)loading";
return; return;
@ -23,6 +35,11 @@ void ApiModel::reload() {
QUrlQuery query; QUrlQuery query;
if (m_limit >= 0) { if (m_limit >= 0) {
query.addQueryItem("Limit", QString::number(m_limit)); query.addQueryItem("Limit", QString::number(m_limit));
} else {
query.addQueryItem("Limit", QString::number(DEFAULT_LIMIT));
}
if (m_startIndex > 0) {
query.addQueryItem("StartIndex", QString::number(m_startIndex));
} }
if (!m_parentId.isEmpty()) { if (!m_parentId.isEmpty()) {
query.addQueryItem("ParentId", m_parentId); query.addQueryItem("ParentId", m_parentId);
@ -45,10 +62,11 @@ void ApiModel::reload() {
if (m_recursive) { if (m_recursive) {
query.addQueryItem("Recursive", "true"); query.addQueryItem("Recursive", "true");
} }
addQueryParameters(query);
QNetworkReply *rep = m_apiClient->get(m_path, query); QNetworkReply *rep = m_apiClient->get(m_path, query);
connect(rep, &QNetworkReply::finished, this, [this, rep]() { connect(rep, &QNetworkReply::finished, this, [this, type, rep]() {
QJsonDocument doc = QJsonDocument::fromJson(rep->readAll()); QJsonDocument doc = QJsonDocument::fromJson(rep->readAll());
if (m_subfield.trimmed().isEmpty()) { if (!m_hasRecordResponse) {
if (!doc.isArray()) { if (!doc.isArray()) {
qWarning() << "Object is not an array!"; qWarning() << "Object is not an array!";
this->setStatus(Error); this->setStatus(Error);
@ -62,19 +80,45 @@ void ApiModel::reload() {
return; return;
} }
QJsonObject obj = doc.object(); QJsonObject obj = doc.object();
if (!obj.contains(m_subfield)) { if (!obj.contains("Items")) {
qWarning() << "Object doesn't contain required subfield!"; qWarning() << "Object doesn't contain items!";
this->setStatus(Error); this->setStatus(Error);
return; return;
} }
if (!obj[m_subfield].isArray()) { if (m_limit < 0) {
qWarning() << "Object's subfield is not an array!"; // Javascript is beautiful
if (obj.contains("TotalRecordCount") && obj["TotalRecordCount"].isDouble()) {
m_totalRecordCount = obj["TotalRecordCount"].toInt();
m_startIndex += DEFAULT_LIMIT;
} else {
qWarning() << "Record-response does not have a total record count";
this->setStatus(Error);
return;
}
}
if (!obj["Items"].isArray()) {
qWarning() << "Items is not an array!";
this->setStatus(Error); this->setStatus(Error);
return; return;
} }
this->m_array = obj[m_subfield].toArray(); QJsonArray items = obj["Items"].toArray();
switch(type) {
case RELOAD:
this->m_array = items;
break;
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
foreach (const QJsonValue &val, items) {
m_array.append(val);
}
this->endInsertRows();
break;
}
}
if (type == RELOAD) {
generateFields();
} }
generateFields();
this->setStatus(Ready); this->setStatus(Ready);
rep->deleteLater(); rep->deleteLater();
}); });
@ -123,6 +167,32 @@ QVariant ApiModel::data(const QModelIndex &index, int role) const {
return QVariant(); return QVariant();
} }
bool ApiModel::canFetchMore(const QModelIndex &parent) const {
if (parent.isValid()) return false;
switch(m_status) {
case Uninitialised:
case Loading:
return false;
default:
break;
}
if (m_limit < 0) {
return m_startIndex <= m_totalRecordCount;
} else {
return false;
}
}
void ApiModel::fetchMore(const QModelIndex &parent) {
if (parent.isValid()) return;
load(LOAD_MORE);
}
void ApiModel::addQueryParameters(QUrlQuery &query) { Q_UNUSED(query)}
void registerModels(const char *URI) { void registerModels(const char *URI) {
qmlRegisterUncreatableType<ApiModel>(URI, 1, 0, "ApiModel", "Is enum and base class"); qmlRegisterUncreatableType<ApiModel>(URI, 1, 0, "ApiModel", "Is enum and base class");
qmlRegisterUncreatableType<SortOrder>(URI, 1, 0, "SortOrder", "Is enum"); qmlRegisterUncreatableType<SortOrder>(URI, 1, 0, "SortOrder", "Is enum");

View file

@ -66,7 +66,8 @@ public:
Uninitialised, Uninitialised,
Loading, Loading,
Ready, Ready,
Error Error,
LoadingMore
}; };
Q_ENUM(ModelStatus) Q_ENUM(ModelStatus)
@ -81,19 +82,24 @@ public:
* @code{.json} * @code{.json}
* [{...}, {...}, {...}] * [{...}, {...}, {...}]
* @endcode * @endcode
* subfield should be left empty *
* or
* @code{.json}
* {...}
* @endcode
* responseHasRecords should be false
* *
* If the response looks something like this: * If the response looks something like this:
* @code{.json} * @code{.json}
* { * {
* "offset": 0, * "Offset": 0,
* "count": 20, * "Count": 20,
* "data": [{...}, {...}, {...}, ..., {...}] * "Items": [{...}, {...}, {...}, ..., {...}]
* } * }
* @endcode * @endcode
* Subfield should be set to "data" in this example. * responseHasRecords should be true
*/ */
explicit ApiModel(QString path, QString subfield, bool passUserId = false, QObject *parent = nullptr); 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)
Q_PROPERTY(ModelStatus status READ status NOTIFY statusChanged) Q_PROPERTY(ModelStatus status READ status NOTIFY statusChanged)
@ -109,16 +115,19 @@ public:
// Path properties // Path properties
Q_PROPERTY(QString show MEMBER m_show NOTIFY showChanged) Q_PROPERTY(QString show MEMBER m_show NOTIFY showChanged)
// Standard QAbstractItemModel overrides
int rowCount(const QModelIndex &index) const override { int rowCount(const QModelIndex &index) const override {
if (!index.isValid()) return m_array.size(); if (!index.isValid()) return m_array.size();
return 0; return 0;
} }
QHash<int, QByteArray> roleNames() const override { return m_roles; } QHash<int, QByteArray> roleNames() const override { return m_roles; }
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
ModelStatus status() const { return m_status; } ModelStatus status() const { return m_status; }
// Helper methods
template<typename QEnum> template<typename QEnum>
QString enumToString (const QEnum anEnum) { return QVariant::fromValue(anEnum).toString(); } QString enumToString (const QEnum anEnum) { return QVariant::fromValue(anEnum).toString(); }
@ -130,6 +139,7 @@ public:
} }
return result; return result;
} }
signals: signals:
void statusChanged(ModelStatus newStatus); void statusChanged(ModelStatus newStatus);
void limitChanged(int newLimit); void limitChanged(int newLimit);
@ -146,18 +156,39 @@ public slots:
*/ */
void reload(); void reload();
protected: protected:
enum LoadType {
RELOAD,
LOAD_MORE
};
void load(LoadType loadType);
/**
* @brief Adds parameters to the query
* @param query The query to add parameters to
*
* This method is intended to be overrided by subclasses. It gets called
* before a request is made to the server and can be used to enable
* query types specific for a certain model to be available.
*/
virtual void addQueryParameters(QUrlQuery &query);
ApiClient *m_apiClient = nullptr; ApiClient *m_apiClient = nullptr;
ModelStatus m_status = Uninitialised; ModelStatus m_status = Uninitialised;
QString m_path; QString m_path;
QString m_subfield;
QJsonArray m_array; QJsonArray m_array;
bool m_hasRecordResponse;
// Path properties // Path properties
QString m_show; QString m_show;
// Query properties // Query/record controlling properties
int m_limit = -1; int m_limit = -1;
int m_startIndex = 0;
int m_totalRecordCount = 0;
const int DEFAULT_LIMIT = 100;
// Query properties
bool m_addUserId = false; bool m_addUserId = false;
QString m_parentId; QString m_parentId;
QString m_seasonId; QString m_seasonId;
@ -167,7 +198,6 @@ protected:
bool m_recursive; bool m_recursive;
QHash<int, QByteArray> m_roles; QHash<int, QByteArray> m_roles;
//QHash<QByteArray, int> m_reverseRoles;
void setStatus(ModelStatus newStatus) { void setStatus(ModelStatus newStatus) {
this->m_status = newStatus; this->m_status = newStatus;
@ -200,24 +230,24 @@ public:
class UserItemModel : public ApiModel { class UserItemModel : public ApiModel {
public: public:
explicit UserItemModel (QObject *parent = nullptr) explicit UserItemModel (QObject *parent = nullptr)
: ApiModel ("/Users/{{user}}/Items", "Items", false, parent) {} : ApiModel ("/Users/{{user}}/Items", true, false, parent) {}
}; };
class UserItemLatestModel : public ApiModel { class UserItemLatestModel : public ApiModel {
public: public:
explicit UserItemLatestModel (QObject *parent = nullptr) explicit UserItemLatestModel (QObject *parent = nullptr)
: ApiModel ("/Users/{{user}}/Items/Latest", "", false, parent) {} : ApiModel ("/Users/{{user}}/Items/Latest", false, false, parent) {}
}; };
class ShowSeasonsModel : public ApiModel { class ShowSeasonsModel : public ApiModel {
public: public:
explicit ShowSeasonsModel (QObject *parent = nullptr) explicit ShowSeasonsModel (QObject *parent = nullptr)
: ApiModel ("/Shows/{{show}}/Seasons", "Items", true, parent) {} : ApiModel ("/Shows/{{show}}/Seasons", true, true, parent) {}
}; };
class ShowEpisodesModel : public ApiModel { class ShowEpisodesModel : public ApiModel {
public: public:
explicit ShowEpisodesModel (QObject *parent = nullptr) explicit ShowEpisodesModel (QObject *parent = nullptr)
: ApiModel ("/Shows/{{show}}/Episodes", "Items", true, parent) {} : ApiModel ("/Shows/{{show}}/Episodes", true, true, parent) {}
}; };