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:
parent
1ba7f6f8ef
commit
5ea17070fe
|
@ -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 \
|
||||||
|
|
5
qml/components/itemdetails/CollectionFolder.qml
Normal file
5
qml/components/itemdetails/CollectionFolder.qml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import QtQuick 2.0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
|
||||||
|
}
|
|
@ -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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
load(RELOAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApiModel::load(LoadType type) {
|
||||||
|
qDebug() << (type == RELOAD ? "RELOAD" : "LOAD_MORE");
|
||||||
|
switch(type) {
|
||||||
|
case RELOAD:
|
||||||
this->setStatus(Loading);
|
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);
|
this->setStatus(Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this->m_array = obj[m_subfield].toArray();
|
|
||||||
}
|
}
|
||||||
|
if (!obj["Items"].isArray()) {
|
||||||
|
qWarning() << "Items is not an array!";
|
||||||
|
this->setStatus(Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
|
|
@ -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) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue