mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-12-22 22:15:17 +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/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 \
|
||||
|
|
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
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#include "credentialmanager.h"
|
||||
|
||||
CredentialsManager * CredentialsManager::getInstance(QObject *parent) {
|
||||
CredentialsManager * CredentialsManager::newInstance(QObject *parent) {
|
||||
return new FallbackCredentialsManager(parent);
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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) {}
|
||||
};
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue