mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-11-22 09:15:18 +00:00
Resolved remaining issues with ApiModel
This commit is contained in:
parent
89fef6d7f4
commit
9abee12658
|
@ -9,6 +9,7 @@ set(CMAKE_CXX_STANDARD 17)
|
||||||
# Options
|
# Options
|
||||||
option(PLATFORM_SAILFISHOS "Build SailfishOS version of application" OFF)
|
option(PLATFORM_SAILFISHOS "Build SailfishOS version of application" OFF)
|
||||||
option(PLATFORM_QTQUICK "Build QtQuick version of application" ON)
|
option(PLATFORM_QTQUICK "Build QtQuick version of application" ON)
|
||||||
|
option(BUILD_PRECOMPILED_HEADERS "Build with precompiled headers for faster compile times when doing a full rebuild, at the cost of slower incremental builds whenever a header file is changed" OFF)
|
||||||
|
|
||||||
if (NOT SAILFIN_VERSION)
|
if (NOT SAILFIN_VERSION)
|
||||||
set(SAILFIN_VERSION "1.0.0")
|
set(SAILFIN_VERSION "1.0.0")
|
||||||
|
|
|
@ -56,7 +56,9 @@ endif()
|
||||||
add_library(JellyfinQt ${JellyfinQt_SOURCES} ${JellyfinQt_HEADERS})
|
add_library(JellyfinQt ${JellyfinQt_SOURCES} ${JellyfinQt_HEADERS})
|
||||||
|
|
||||||
if(${CMAKE_VERSION} VERSION_GREATER "3.16.0")
|
if(${CMAKE_VERSION} VERSION_GREATER "3.16.0")
|
||||||
# target_precompile_headers(JellyfinQt PRIVATE ${JellyfinQt_HEADERS})
|
if(BUILD_PRECOMPILED_HEADERS)
|
||||||
|
target_precompile_headers(JellyfinQt PRIVATE ${JellyfinQt_HEADERS})
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_include_directories(JellyfinQt PUBLIC "include")
|
target_include_directories(JellyfinQt PUBLIC "include")
|
||||||
|
|
|
@ -48,13 +48,27 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A brief description of this file:
|
||||||
|
*
|
||||||
|
* The reason why all of this is this complex is because Qt MOC's lack of support for template classes
|
||||||
|
* with Q_OBJECT. To work around this, "base classes", such as BaseModelLoader, are created which contain
|
||||||
|
* all functionallity required by Q_OBJECT. This class is extended by the templated class, which in turn
|
||||||
|
* is extended once more, so it can be registered for the QML engine.
|
||||||
|
*
|
||||||
|
* The loading of the data has beens separated from the QAbstractModels into ViewModel::Loaders
|
||||||
|
* to allow for loading over the network, loading from the cache and so on, without the QAbstractModel
|
||||||
|
* knowing anything about how it's done, except for the parameters it can pass to the loader and the result.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
class BaseModelLoader : public QObject, public QQmlParserStatus {
|
class BaseModelLoader : public QObject, public QQmlParserStatus {
|
||||||
Q_INTERFACES(QQmlParserStatus)
|
Q_INTERFACES(QQmlParserStatus)
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit BaseModelLoader(QObject *parent = nullptr);
|
explicit BaseModelLoader(QObject *parent = nullptr);
|
||||||
Q_PROPERTY(ApiClient *apiClient READ apiClient WRITE setApiClient NOTIFY apiClientChanged)
|
Q_PROPERTY(ApiClient *apiClient READ apiClient WRITE setApiClient NOTIFY apiClientChanged)
|
||||||
Q_PROPERTY(ViewModel::ModelStatus status READ status NOTIFY statusChanged)
|
Q_PROPERTY(Jellyfin::ViewModel::ModelStatusClass::Value status READ status NOTIFY statusChanged)
|
||||||
Q_PROPERTY(int limit READ limit WRITE setLimit NOTIFY limitChanged)
|
Q_PROPERTY(int limit READ limit WRITE setLimit NOTIFY limitChanged)
|
||||||
Q_PROPERTY(bool autoReload READ autoReload WRITE setAutoReload NOTIFY autoReloadChanged)
|
Q_PROPERTY(bool autoReload READ autoReload WRITE setAutoReload NOTIFY autoReloadChanged)
|
||||||
|
|
||||||
|
@ -123,6 +137,15 @@ protected:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @brief Determines if this model is able to reload.
|
||||||
|
*
|
||||||
|
* The default implementation checks if the user is authenticated,
|
||||||
|
* and the model is not reloading. If overriding this method, please
|
||||||
|
* call this method as well in determining if the model should be reloadable.
|
||||||
|
*
|
||||||
|
* @return True if the model can reload, false otherwise.
|
||||||
|
*/
|
||||||
virtual bool canReload() const;
|
virtual bool canReload() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -144,9 +167,8 @@ public:
|
||||||
}
|
}
|
||||||
m_startIndex = 0;
|
m_startIndex = 0;
|
||||||
m_totalRecordCount = -1;
|
m_totalRecordCount = -1;
|
||||||
this->setStatus(ViewModel::ModelStatus::Loading);
|
|
||||||
emitModelShouldClear();
|
emitModelShouldClear();
|
||||||
loadMore(0, -1);
|
loadMore(0, -1, ViewModel::ModelStatus::Loading);
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadMore() {
|
void loadMore() {
|
||||||
|
@ -154,8 +176,7 @@ public:
|
||||||
qDebug() << "Cannot yet reload ApiModel: canReload() returned false.";
|
qDebug() << "Cannot yet reload ApiModel: canReload() returned false.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this->setStatus(ViewModel::ModelStatus::LoadingMore);
|
loadMore(m_startIndex, m_limit, ViewModel::ModelStatus::LoadingMore);
|
||||||
loadMore(m_startIndex, m_limit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual bool canLoadMore() const {
|
virtual bool canLoadMore() const {
|
||||||
|
@ -177,8 +198,10 @@ protected:
|
||||||
*
|
*
|
||||||
* @param offset The offset to start loading items from
|
* @param offset The offset to start loading items from
|
||||||
* @param limit The maximum amount of items to load.
|
* @param limit The maximum amount of items to load.
|
||||||
|
* @param suggestedStatus The suggested status this model should take on if it is able to load (more).
|
||||||
|
* Either LOADING or LOAD_MORE.
|
||||||
*/
|
*/
|
||||||
virtual void loadMore(int offset, int limit) = 0;
|
virtual void loadMore(int offset, int limit, ViewModel::ModelStatus suggestedStatus) = 0;
|
||||||
void updatePosition(int startIndex, int totalRecordCount) {
|
void updatePosition(int startIndex, int totalRecordCount) {
|
||||||
m_startIndex = startIndex;
|
m_startIndex = startIndex;
|
||||||
m_totalRecordCount = totalRecordCount;
|
m_totalRecordCount = totalRecordCount;
|
||||||
|
@ -209,15 +232,20 @@ void setRequestLimit(R ¶meters, int limit) {
|
||||||
Q_UNIMPLEMENTED();
|
Q_UNIMPLEMENTED();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return True if able to set the startIndex, false otherwise.
|
||||||
|
*/
|
||||||
template <class P>
|
template <class P>
|
||||||
void setRequestStartIndex(P ¶meters, int startIndex) {
|
bool setRequestStartIndex(P ¶meters, int startIndex) {
|
||||||
Q_UNUSED(parameters)
|
Q_UNUSED(parameters)
|
||||||
Q_UNUSED(startIndex)
|
Q_UNUSED(startIndex)
|
||||||
Q_UNIMPLEMENTED();
|
Q_UNIMPLEMENTED();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Template for implementing a loader for the given type, response and parameters
|
* Template for implementing a loader for the given type, response and parameters using Jellyfin::Support:Loaders.
|
||||||
|
*
|
||||||
* @tparam T type of which this loader should load a list of
|
* @tparam T type of which this loader should load a list of
|
||||||
* @tparam D type of the DTO which can be converted into T using T(const D&, ApiClient*);
|
* @tparam D type of the DTO which can be converted into T using T(const D&, ApiClient*);
|
||||||
* @tparam R type of the deserialized loader response
|
* @tparam R type of the deserialized loader response
|
||||||
|
@ -231,19 +259,25 @@ public:
|
||||||
QObject::connect(&m_futureWatcher, &QFutureWatcher<QList<T>>::finished, this, &BaseModelLoader::futureReady);
|
QObject::connect(&m_futureWatcher, &QFutureWatcher<QList<T>>::finished, this, &BaseModelLoader::futureReady);
|
||||||
}
|
}
|
||||||
protected:
|
protected:
|
||||||
void loadMore(int offset, int limit) override {
|
void loadMore(int offset, int limit, ViewModel::ModelStatus suggestedModelStatus) override {
|
||||||
// This method should only be callable on one thread.
|
// This method should only be callable on one thread.
|
||||||
// If futureWatcher's future is running, this method should not be called again.
|
// If futureWatcher's future is running, this method should not be called again.
|
||||||
if (m_futureWatcher.isRunning()) return;
|
if (m_futureWatcher.isRunning()) return;
|
||||||
// Set an invalid result.
|
// Set an invalid result.
|
||||||
this->m_result = { QList<T>(), -1 };
|
this->m_result = { QList<T>(), -1 };
|
||||||
|
|
||||||
|
if (!setRequestStartIndex<P>(this->m_parameters, offset)
|
||||||
|
&& suggestedModelStatus == ViewModel::ModelStatus::LoadingMore) {
|
||||||
|
// This loader's parameters does not setting a starting index,
|
||||||
|
// meaning loadMore is not supported.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRequestLimit<P>(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
|
// We never want to set this while the loader is running, hence the Mutex and setting it here
|
||||||
// instead when Loader::setApiClient is called.
|
// instead when Loader::setApiClient is called.
|
||||||
this->m_loader->setApiClient(this->m_apiClient);
|
this->m_loader->setApiClient(this->m_apiClient);
|
||||||
setRequestStartIndex<P>(this->m_parameters, offset);
|
|
||||||
setRequestLimit<P>(this->m_parameters, limit);
|
|
||||||
|
|
||||||
this->m_loader->setParameters(this->m_parameters);
|
this->m_loader->setParameters(this->m_parameters);
|
||||||
this->m_loader->prepareLoad();
|
this->m_loader->prepareLoad();
|
||||||
QFuture<std::optional<R>> future = QtConcurrent::run(this->m_loader.data(), &Support::Loader<R, P>::load);
|
QFuture<std::optional<R>> future = QtConcurrent::run(this->m_loader.data(), &Support::Loader<R, P>::load);
|
||||||
|
@ -268,10 +302,13 @@ protected:
|
||||||
} catch (Support::LoadException e) {
|
} catch (Support::LoadException e) {
|
||||||
qWarning() << "Exception while loading: " << e.what();
|
qWarning() << "Exception while loading: " << e.what();
|
||||||
this->setStatus(ViewModel::ModelStatus::Error);
|
this->setStatus(ViewModel::ModelStatus::Error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
QList<D> records = extractRecords<D, R>(result);
|
QList<D> records = extractRecords<D, R>(result);
|
||||||
int totalRecordCount = extractTotalRecordCount<R>(result);
|
int totalRecordCount = extractTotalRecordCount<R>(result);
|
||||||
|
qDebug() << "Total record count: " << totalRecordCount << ", records in request: " << records.size();
|
||||||
// If totalRecordCount < 0, it is not supported for this endpoint
|
// If totalRecordCount < 0, it is not supported for this endpoint
|
||||||
if (totalRecordCount < 0) {
|
if (totalRecordCount < 0) {
|
||||||
totalRecordCount = records.size();
|
totalRecordCount = records.size();
|
||||||
|
@ -281,10 +318,11 @@ protected:
|
||||||
|
|
||||||
// Convert the DTOs into models
|
// Convert the DTOs into models
|
||||||
for (int i = 0; i < records.size(); i++) {
|
for (int i = 0; i < records.size(); i++) {
|
||||||
models[i] = T(records[i], m_loader->apiClient());
|
models.append(T(records[i], m_loader->apiClient()));
|
||||||
}
|
}
|
||||||
this->setStatus(ViewModel::ModelStatus::Ready);
|
this->setStatus(ViewModel::ModelStatus::Ready);
|
||||||
this->m_result = { models, totalRecordCount};
|
this->m_result = { models, this->m_startIndex};
|
||||||
|
this->m_startIndex += totalRecordCount;
|
||||||
this->emitItemsLoaded();
|
this->emitItemsLoaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -408,7 +446,7 @@ public:
|
||||||
void append(T &object) { insert(size(), object); }
|
void append(T &object) { insert(size(), object); }
|
||||||
void append(QList<T> &objects) {
|
void append(QList<T> &objects) {
|
||||||
int index = size();
|
int index = size();
|
||||||
this->beginInsertRows(QModelIndex(), index, index + objects.size());
|
this->beginInsertRows(QModelIndex(), index, index + objects.size() - 1);
|
||||||
m_array.append(objects);
|
m_array.append(objects);
|
||||||
this->endInsertRows();
|
this->endInsertRows();
|
||||||
};
|
};
|
||||||
|
@ -486,6 +524,7 @@ protected:
|
||||||
void loadingFinished() override {
|
void loadingFinished() override {
|
||||||
Q_ASSERT(m_loader != nullptr);
|
Q_ASSERT(m_loader != nullptr);
|
||||||
std::pair<QList<T>, int> result = m_loader->result();
|
std::pair<QList<T>, int> result = m_loader->result();
|
||||||
|
qDebug() << "Results loaded: index: " << result.second << ", count: " << result.first.size();
|
||||||
if (result.second == -1) {
|
if (result.second == -1) {
|
||||||
clear();
|
clear();
|
||||||
} else if (result.second == m_array.size()) {
|
} else if (result.second == m_array.size()) {
|
||||||
|
|
|
@ -42,36 +42,6 @@ private:
|
||||||
};
|
};
|
||||||
|
|
||||||
using ModelStatus = ModelStatusClass::Value;
|
using ModelStatus = ModelStatusClass::Value;
|
||||||
|
|
||||||
|
|
||||||
class ModelStatusTest : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit ModelStatusTest(QObject *parent = nullptr) : QObject(parent) {
|
|
||||||
m_timer.setInterval(500);
|
|
||||||
connect(&m_timer, &QTimer::timeout, this, &ModelStatusTest::rotateStatus);
|
|
||||||
m_timer.setSingleShot(false);
|
|
||||||
m_timer.start();
|
|
||||||
}
|
|
||||||
Q_PROPERTY(ModelStatus status READ status WRITE setStatus NOTIFY statusChanged)
|
|
||||||
|
|
||||||
ModelStatus status() const { return m_status; }
|
|
||||||
|
|
||||||
void setStatus(ModelStatus newStatus) {
|
|
||||||
m_status = newStatus;
|
|
||||||
emit statusChanged();
|
|
||||||
}
|
|
||||||
signals:
|
|
||||||
void statusChanged();
|
|
||||||
private slots:
|
|
||||||
void rotateStatus() {
|
|
||||||
setStatus(static_cast<ModelStatus>((m_status + 1) % ModelStatus::LoadingMore));
|
|
||||||
}
|
|
||||||
private:
|
|
||||||
ModelStatus m_status = ModelStatus::Uninitialised;
|
|
||||||
QTimer m_timer;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
}
|
||||||
} // NS Jellyfin
|
} // NS Jellyfin
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,7 @@ void BaseModelLoader::componentComplete() {
|
||||||
|
|
||||||
void BaseModelLoader::autoReloadIfNeeded() {
|
void BaseModelLoader::autoReloadIfNeeded() {
|
||||||
if (m_autoReload && canReload()) {
|
if (m_autoReload && canReload()) {
|
||||||
|
qDebug() << "reloading due to 'autoReloadIfNeeded()'";
|
||||||
emit reloadWanted();
|
emit reloadWanted();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,28 +75,39 @@ void BaseModelLoader::setAutoReload(bool newAutoReload) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BaseModelLoader::canReload() const {
|
bool BaseModelLoader::canReload() const {
|
||||||
return m_apiClient != nullptr && (!m_needsAuthentication || m_apiClient->authenticated());
|
return m_apiClient != nullptr
|
||||||
|
// 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())
|
||||||
|
// Only allow for a reload if this model is ready or uninitialised.
|
||||||
|
&& (m_status == ViewModel::ModelStatus::Ready
|
||||||
|
|| m_status == ViewModel::ModelStatus::Uninitialised);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BaseApiModel::reload() {
|
void BaseApiModel::reload() {
|
||||||
qWarning() << " BaseApiModel slot called instead of overloaded method";
|
qWarning() << " BaseApiModel slot called instead of overloaded method";
|
||||||
}
|
}
|
||||||
|
|
||||||
void setStartIndex(Loader::GetUserViewsParams ¶ms, int startIndex) {
|
template <>
|
||||||
|
bool setRequestStartIndex(Loader::GetUserViewsParams ¶ms, int startIndex) {
|
||||||
// Not supported
|
// Not supported
|
||||||
Q_UNUSED(params)
|
Q_UNUSED(params)
|
||||||
Q_UNUSED(startIndex)
|
Q_UNUSED(startIndex)
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLimit(Loader::GetUserViewsParams ¶ms, int limit) {
|
template <>
|
||||||
|
void setRequestLimit(Loader::GetUserViewsParams ¶ms, int limit) {
|
||||||
Q_UNUSED(params)
|
Q_UNUSED(params)
|
||||||
Q_UNUSED(limit)
|
Q_UNUSED(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
QList<DTO::BaseItemDto> extractRecords(const DTO::BaseItemDtoQueryResult &result) {
|
QList<DTO::BaseItemDto> extractRecords(const DTO::BaseItemDtoQueryResult &result) {
|
||||||
return result.items();
|
return result.items();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
int extractTotalRecordCount(const DTO::BaseItemDtoQueryResult &result) {
|
int extractTotalRecordCount(const DTO::BaseItemDtoQueryResult &result) {
|
||||||
return result.totalRecordCount();
|
return result.totalRecordCount();
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,6 @@ void registerTypes(const char *uri) {
|
||||||
qmlRegisterUncreatableType<ViewModel::LoaderBase>(uri, 1, 0, "LoaderBase", "Use on eof its subclasses");
|
qmlRegisterUncreatableType<ViewModel::LoaderBase>(uri, 1, 0, "LoaderBase", "Use on eof its subclasses");
|
||||||
|
|
||||||
qmlRegisterUncreatableType<ViewModel::Item>(uri, 1, 0, "Item", "Acquire one via ItemLoader or exposed properties");
|
qmlRegisterUncreatableType<ViewModel::Item>(uri, 1, 0, "Item", "Acquire one via ItemLoader or exposed properties");
|
||||||
qmlRegisterType<ViewModel::ModelStatusTest>(uri, 1, 0, "ModelStatusTest");
|
|
||||||
qmlRegisterType<ViewModel::ItemLoader>(uri, 1, 0, "ItemLoader");
|
qmlRegisterType<ViewModel::ItemLoader>(uri, 1, 0, "ItemLoader");
|
||||||
qmlRegisterType<ViewModel::ItemModel>(uri, 1, 0, "ItemModel");
|
qmlRegisterType<ViewModel::ItemModel>(uri, 1, 0, "ItemModel");
|
||||||
qmlRegisterType<ViewModel::UserViewsLoader>(uri, 1, 0, "UsersViewLoader");
|
qmlRegisterType<ViewModel::UserViewsLoader>(uri, 1, 0, "UsersViewLoader");
|
||||||
|
|
|
@ -19,7 +19,11 @@
|
||||||
#include "JellyfinQt/viewmodel/itemmodel.h"
|
#include "JellyfinQt/viewmodel/itemmodel.h"
|
||||||
|
|
||||||
#define JF_CASE(roleName) case roleName: \
|
#define JF_CASE(roleName) case roleName: \
|
||||||
return QVariant(item.roleName());
|
try { \
|
||||||
|
return QVariant(item.roleName()); \
|
||||||
|
} catch(std::bad_optional_access e) { \
|
||||||
|
return QVariant(); \
|
||||||
|
}
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
|
|
||||||
|
|
|
@ -19,25 +19,11 @@ Page {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
|
||||||
id: simpleLog
|
|
||||||
text: "Simple log: \n"
|
|
||||||
}
|
|
||||||
|
|
||||||
J.ItemModel {
|
J.ItemModel {
|
||||||
id: mediaLibraryModel
|
id: mediaLibraryModel
|
||||||
loader: J.UsersViewLoader {
|
loader: J.UsersViewLoader {
|
||||||
id: mediaLibraryModelLoader
|
id: mediaLibraryModelLoader
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
onStatusChanged: {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
J.ModelStatusTest {
|
|
||||||
status: J.ModelStatus.Uninitialized
|
|
||||||
onStatusChanged: {
|
|
||||||
simpleLog.text += new Date().toString() + ": " + status + "\n"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +44,7 @@ Page {
|
||||||
limit: 16
|
limit: 16
|
||||||
}*/
|
}*/
|
||||||
Label {
|
Label {
|
||||||
text: model.name
|
text: model.name ? model.name : "<Model without name>"
|
||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
|
|
Loading…
Reference in a new issue