1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-05-05 14:02:43 +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/UserGridDelegate.qml \
qml/components/VideoPlayer.qml \
qml/components/itemdetails/CollectionFolder.qml \
qml/components/itemdetails/EpisodeDetails.qml \
qml/components/itemdetails/FilmDetails.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
source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags["Primary"], "Primary", {"maxHeight": row1.height})
fillMode: Image.PreserveAspectCrop
Component.onCompleted: console.log(JSON.stringify(model.imageTags))
}
}
@ -105,7 +104,6 @@ CoverBackground {
width: height
source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags["Primary"], "Primary", {"maxHeight": row1.height})
fillMode: Image.PreserveAspectCrop
Component.onCompleted: console.log(JSON.stringify(model.imageTags))
}
}

View file

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

View file

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

View file

@ -7,6 +7,13 @@
#include <QSettings>
#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 {
Q_OBJECT
public:
@ -61,9 +68,11 @@ public:
/**
* @brief Retrieves an implementation which can store this token.
* @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).
*/
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.

View file

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

View file

@ -1,15 +1,27 @@
#include "jellyfinapimodel.h"
namespace Jellyfin {
ApiModel::ApiModel(QString path, QString subfield, bool addUserId, QObject *parent)
ApiModel::ApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent)
: QAbstractListModel (parent),
m_path(path),
m_subfield(subfield),
m_hasRecordResponse(hasRecordResponse),
m_addUserId(addUserId){
}
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) {
qWarning() << "Please set the apiClient property before (re)loading";
return;
@ -23,6 +35,11 @@ void ApiModel::reload() {
QUrlQuery query;
if (m_limit >= 0) {
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()) {
query.addQueryItem("ParentId", m_parentId);
@ -45,10 +62,11 @@ void ApiModel::reload() {
if (m_recursive) {
query.addQueryItem("Recursive", "true");
}
addQueryParameters(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());
if (m_subfield.trimmed().isEmpty()) {
if (!m_hasRecordResponse) {
if (!doc.isArray()) {
qWarning() << "Object is not an array!";
this->setStatus(Error);
@ -62,19 +80,45 @@ void ApiModel::reload() {
return;
}
QJsonObject obj = doc.object();
if (!obj.contains(m_subfield)) {
qWarning() << "Object doesn't contain required subfield!";
if (!obj.contains("Items")) {
qWarning() << "Object doesn't contain items!";
this->setStatus(Error);
return;
}
if (!obj[m_subfield].isArray()) {
qWarning() << "Object's subfield is not an array!";
if (m_limit < 0) {
// 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);
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);
rep->deleteLater();
});
@ -123,6 +167,32 @@ QVariant ApiModel::data(const QModelIndex &index, int role) const {
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) {
qmlRegisterUncreatableType<ApiModel>(URI, 1, 0, "ApiModel", "Is enum and base class");
qmlRegisterUncreatableType<SortOrder>(URI, 1, 0, "SortOrder", "Is enum");

View file

@ -66,7 +66,8 @@ public:
Uninitialised,
Loading,
Ready,
Error
Error,
LoadingMore
};
Q_ENUM(ModelStatus)
@ -81,19 +82,24 @@ public:
* @code{.json}
* [{...}, {...}, {...}]
* @endcode
* subfield should be left empty
*
* or
* @code{.json}
* {...}
* @endcode
* responseHasRecords should be false
*
* If the response looks something like this:
* @code{.json}
* {
* "offset": 0,
* "count": 20,
* "data": [{...}, {...}, {...}, ..., {...}]
* "Offset": 0,
* "Count": 20,
* "Items": [{...}, {...}, {...}, ..., {...}]
* }
* @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(ModelStatus status READ status NOTIFY statusChanged)
@ -109,16 +115,19 @@ public:
// Path properties
Q_PROPERTY(QString show MEMBER m_show NOTIFY showChanged)
// Standard QAbstractItemModel overrides
int rowCount(const QModelIndex &index) const override {
if (!index.isValid()) return m_array.size();
return 0;
}
QHash<int, QByteArray> roleNames() const override { return m_roles; }
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; }
// Helper methods
template<typename QEnum>
QString enumToString (const QEnum anEnum) { return QVariant::fromValue(anEnum).toString(); }
@ -130,6 +139,7 @@ public:
}
return result;
}
signals:
void statusChanged(ModelStatus newStatus);
void limitChanged(int newLimit);
@ -146,18 +156,39 @@ public slots:
*/
void reload();
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;
ModelStatus m_status = Uninitialised;
QString m_path;
QString m_subfield;
QJsonArray m_array;
bool m_hasRecordResponse;
// Path properties
QString m_show;
// Query properties
// Query/record controlling properties
int m_limit = -1;
int m_startIndex = 0;
int m_totalRecordCount = 0;
const int DEFAULT_LIMIT = 100;
// Query properties
bool m_addUserId = false;
QString m_parentId;
QString m_seasonId;
@ -167,7 +198,6 @@ protected:
bool m_recursive;
QHash<int, QByteArray> m_roles;
//QHash<QByteArray, int> m_reverseRoles;
void setStatus(ModelStatus newStatus) {
this->m_status = newStatus;
@ -200,24 +230,24 @@ public:
class UserItemModel : public ApiModel {
public:
explicit UserItemModel (QObject *parent = nullptr)
: ApiModel ("/Users/{{user}}/Items", "Items", false, parent) {}
: ApiModel ("/Users/{{user}}/Items", true, false, parent) {}
};
class UserItemLatestModel : public ApiModel {
public:
explicit UserItemLatestModel (QObject *parent = nullptr)
: ApiModel ("/Users/{{user}}/Items/Latest", "", false, parent) {}
: ApiModel ("/Users/{{user}}/Items/Latest", false, false, parent) {}
};
class ShowSeasonsModel : public ApiModel {
public:
explicit ShowSeasonsModel (QObject *parent = nullptr)
: ApiModel ("/Shows/{{show}}/Seasons", "Items", true, parent) {}
: ApiModel ("/Shows/{{show}}/Seasons", true, true, parent) {}
};
class ShowEpisodesModel : public ApiModel {
public:
explicit ShowEpisodesModel (QObject *parent = nullptr)
: ApiModel ("/Shows/{{show}}/Episodes", "Items", true, parent) {}
: ApiModel ("/Shows/{{show}}/Episodes", true, true, parent) {}
};