From 9abee126583a8ad3aa81bcc7c85241b0ab584213 Mon Sep 17 00:00:00 2001 From: Chris Josten Date: Mon, 29 Mar 2021 17:10:25 +0200 Subject: [PATCH] Resolved remaining issues with ApiModel --- CMakeLists.txt | 1 + core/CMakeLists.txt | 4 +- core/include/JellyfinQt/apimodel.h | 69 +++++++++++++++---- .../JellyfinQt/viewmodel/modelstatus.h | 30 -------- core/src/apimodel.cpp | 18 ++++- core/src/jellyfin.cpp | 1 - core/src/viewmodel/itemmodel.cpp | 6 +- qtquick/qml/pages/MainPage.qml | 16 +---- 8 files changed, 79 insertions(+), 66 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c75656..25a2be4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,7 @@ set(CMAKE_CXX_STANDARD 17) # Options option(PLATFORM_SAILFISHOS "Build SailfishOS version of application" OFF) 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) set(SAILFIN_VERSION "1.0.0") diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index f5db19f..64fa6fc 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -56,7 +56,9 @@ endif() add_library(JellyfinQt ${JellyfinQt_SOURCES} ${JellyfinQt_HEADERS}) 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() target_include_directories(JellyfinQt PUBLIC "include") diff --git a/core/include/JellyfinQt/apimodel.h b/core/include/JellyfinQt/apimodel.h index de38f3c..5fef3cd 100644 --- a/core/include/JellyfinQt/apimodel.h +++ b/core/include/JellyfinQt/apimodel.h @@ -48,13 +48,27 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 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 { Q_INTERFACES(QQmlParserStatus) Q_OBJECT public: explicit BaseModelLoader(QObject *parent = nullptr); 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(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; }; @@ -144,9 +167,8 @@ public: } m_startIndex = 0; m_totalRecordCount = -1; - this->setStatus(ViewModel::ModelStatus::Loading); emitModelShouldClear(); - loadMore(0, -1); + loadMore(0, -1, ViewModel::ModelStatus::Loading); } void loadMore() { @@ -154,8 +176,7 @@ public: qDebug() << "Cannot yet reload ApiModel: canReload() returned false."; return; } - this->setStatus(ViewModel::ModelStatus::LoadingMore); - loadMore(m_startIndex, m_limit); + loadMore(m_startIndex, m_limit, ViewModel::ModelStatus::LoadingMore); } virtual bool canLoadMore() const { @@ -177,8 +198,10 @@ protected: * * @param offset The offset to start loading items from * @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) { m_startIndex = startIndex; m_totalRecordCount = totalRecordCount; @@ -209,15 +232,20 @@ void setRequestLimit(R ¶meters, int limit) { Q_UNIMPLEMENTED(); } +/** + * @return True if able to set the startIndex, false otherwise. + */ template -void setRequestStartIndex(P ¶meters, int startIndex) { +bool setRequestStartIndex(P ¶meters, int startIndex) { Q_UNUSED(parameters) Q_UNUSED(startIndex) 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 D type of the DTO which can be converted into T using T(const D&, ApiClient*); * @tparam R type of the deserialized loader response @@ -231,19 +259,25 @@ public: QObject::connect(&m_futureWatcher, &QFutureWatcher>::finished, this, &BaseModelLoader::futureReady); } 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. // If futureWatcher's future is running, this method should not be called again. if (m_futureWatcher.isRunning()) return; // Set an invalid result. this->m_result = { QList(), -1 }; + if (!setRequestStartIndex

(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

(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 // instead when Loader::setApiClient is called. this->m_loader->setApiClient(this->m_apiClient); - setRequestStartIndex

(this->m_parameters, offset); - setRequestLimit

(this->m_parameters, limit); - this->m_loader->setParameters(this->m_parameters); this->m_loader->prepareLoad(); QFuture> future = QtConcurrent::run(this->m_loader.data(), &Support::Loader::load); @@ -268,10 +302,13 @@ protected: } catch (Support::LoadException e) { qWarning() << "Exception while loading: " << e.what(); this->setStatus(ViewModel::ModelStatus::Error); + return; } + QList records = extractRecords(result); int totalRecordCount = extractTotalRecordCount(result); + qDebug() << "Total record count: " << totalRecordCount << ", records in request: " << records.size(); // If totalRecordCount < 0, it is not supported for this endpoint if (totalRecordCount < 0) { totalRecordCount = records.size(); @@ -281,10 +318,11 @@ protected: // Convert the DTOs into models 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->m_result = { models, totalRecordCount}; + this->m_result = { models, this->m_startIndex}; + this->m_startIndex += totalRecordCount; this->emitItemsLoaded(); } @@ -408,7 +446,7 @@ public: void append(T &object) { insert(size(), object); } void append(QList &objects) { int index = size(); - this->beginInsertRows(QModelIndex(), index, index + objects.size()); + this->beginInsertRows(QModelIndex(), index, index + objects.size() - 1); m_array.append(objects); this->endInsertRows(); }; @@ -486,6 +524,7 @@ protected: void loadingFinished() override { Q_ASSERT(m_loader != nullptr); std::pair, int> result = m_loader->result(); + qDebug() << "Results loaded: index: " << result.second << ", count: " << result.first.size(); if (result.second == -1) { clear(); } else if (result.second == m_array.size()) { diff --git a/core/include/JellyfinQt/viewmodel/modelstatus.h b/core/include/JellyfinQt/viewmodel/modelstatus.h index 924888a..386e680 100644 --- a/core/include/JellyfinQt/viewmodel/modelstatus.h +++ b/core/include/JellyfinQt/viewmodel/modelstatus.h @@ -42,36 +42,6 @@ private: }; 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((m_status + 1) % ModelStatus::LoadingMore)); - } -private: - ModelStatus m_status = ModelStatus::Uninitialised; - QTimer m_timer; -}; - } } // NS Jellyfin diff --git a/core/src/apimodel.cpp b/core/src/apimodel.cpp index 41d8504..90d8186 100644 --- a/core/src/apimodel.cpp +++ b/core/src/apimodel.cpp @@ -46,6 +46,7 @@ void BaseModelLoader::componentComplete() { void BaseModelLoader::autoReloadIfNeeded() { if (m_autoReload && canReload()) { + qDebug() << "reloading due to 'autoReloadIfNeeded()'"; emit reloadWanted(); } } @@ -74,28 +75,39 @@ void BaseModelLoader::setAutoReload(bool newAutoReload) { } 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() { 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 Q_UNUSED(params) 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(limit) } +template <> QList extractRecords(const DTO::BaseItemDtoQueryResult &result) { return result.items(); } +template <> int extractTotalRecordCount(const DTO::BaseItemDtoQueryResult &result) { return result.totalRecordCount(); } diff --git a/core/src/jellyfin.cpp b/core/src/jellyfin.cpp index ced789b..d6ec414 100644 --- a/core/src/jellyfin.cpp +++ b/core/src/jellyfin.cpp @@ -29,7 +29,6 @@ void registerTypes(const char *uri) { qmlRegisterUncreatableType(uri, 1, 0, "LoaderBase", "Use on eof its subclasses"); qmlRegisterUncreatableType(uri, 1, 0, "Item", "Acquire one via ItemLoader or exposed properties"); - qmlRegisterType(uri, 1, 0, "ModelStatusTest"); qmlRegisterType(uri, 1, 0, "ItemLoader"); qmlRegisterType(uri, 1, 0, "ItemModel"); qmlRegisterType(uri, 1, 0, "UsersViewLoader"); diff --git a/core/src/viewmodel/itemmodel.cpp b/core/src/viewmodel/itemmodel.cpp index b15263d..7a5c84c 100644 --- a/core/src/viewmodel/itemmodel.cpp +++ b/core/src/viewmodel/itemmodel.cpp @@ -19,7 +19,11 @@ #include "JellyfinQt/viewmodel/itemmodel.h" #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 { diff --git a/qtquick/qml/pages/MainPage.qml b/qtquick/qml/pages/MainPage.qml index f30c37a..c5b1ba8 100644 --- a/qtquick/qml/pages/MainPage.qml +++ b/qtquick/qml/pages/MainPage.qml @@ -19,25 +19,11 @@ Page { } } - Text { - id: simpleLog - text: "Simple log: \n" - } - J.ItemModel { id: mediaLibraryModel loader: J.UsersViewLoader { id: mediaLibraryModelLoader apiClient: ApiClient - onStatusChanged: { - } - } - } - - J.ModelStatusTest { - status: J.ModelStatus.Uninitialized - onStatusChanged: { - simpleLog.text += new Date().toString() + ": " + status + "\n" } } @@ -58,7 +44,7 @@ Page { limit: 16 }*/ Label { - text: model.name + text: model.name ? model.name : "" } ListView {