/* * Sailfin: a Jellyfin client written using Qt * Copyright (C) 2021 Chris Josten and the Sailfin Contributors. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #ifndef JELLYFIN_DTO_DTO #define JELLYFIN_DTO_DTO #include #include #include #include #include #include #include "../support/loader.h" namespace Jellyfin { namespace ViewModel { /** * @brief An "interface" for a remote data source * * This class is basically a base class for JSON data that can be fetched from over the network. * Subclasses should reimplement reload and call setStatus to update the QML part of the code * appropiatly. */ class LoaderBase : public QObject, public QQmlParserStatus { Q_OBJECT Q_INTERFACES(QQmlParserStatus) public: enum Status { /// The data is unitialized and not loading either. Uninitialised, /// The data is being loaded over the network Loading, /// The data is ready, the properties in this object are up to date. Ready, /// An error has occurred while loading the data. See error() for more details. Error }; Q_ENUM(Status) explicit LoaderBase(QObject *parent = nullptr) : QObject(parent) {} LoaderBase(ApiClient *apiClient, QObject *parent = nullptr) : QObject(parent), m_apiClient(apiClient) {} Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient WRITE setApiClient NOTIFY apiClientChanged STORED false) Q_PROPERTY(Status status READ status NOTIFY statusChanged STORED false) Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged STORED false) Q_PROPERTY(bool autoReload MEMBER m_autoReload NOTIFY autoReloadChanged) Q_PROPERTY(QObject *data READ data NOTIFY dataChanged STORED false) Status status() const { return m_status; } QString errorString() const { return m_errorString; } virtual QObject *data() const { return nullptr; } void setApiClient(ApiClient *newApiClient); void setExtraFields(const QStringList &extraFields); signals: void statusChanged(Status newStatus); void apiClientChanged(ApiClient *newApiClient); void errorStringChanged(QString newErrorString); void autoReloadChanged(bool newAutoReload); void dataChanged(); /** * @brief Convenience signal for status == RemoteData.Ready. */ void ready(); public slots: /** * @brief Overload this method to reimplement the fetching mechanism to * populate the RemoteData with data from the server. * * The default implementation makes a GET request to getDataUrl() and parses the resulting JSON, * which should be enough for most cases. Consider overriding getDataUrl() and * canRelaod() if possible. Manual overrides need to make sure that * they're calling setStatus(Status), setError(QNetworkReply::NetworkError) and * setErrorString() to let the QML side know what this thing is up to. */ virtual void reload() {}; protected: /** * @brief Subclasses should implement this to determine if they can * load data from the server. * * Usage cases include checking if the * required properties, such as the item id are set. */ virtual bool canReload() const; void setStatus(Status newStatus); void setError(QNetworkReply::NetworkError error); void setErrorString(const QString &newErrorString); void classBegin() override { m_isParsing = true; } void componentComplete() override { m_isParsing = false; if (canReload()) { reload(); } } ApiClient *m_apiClient = nullptr; protected: // Returns true if this class is instantiated within QML and is still being parsed. bool isQmlParsing() const { return m_isParsing; } void emitDataChanged(); private: Status m_status = Uninitialised; QString m_errorString; bool m_autoReload = true; bool m_isParsing = false; }; /** * Class representing data from a remote source, meant to be instantiated from QML. * * Subclasses should do the following: * - initialize m_loader with an appropiate loader * - create properties that map to the parameters. * - override canReload() */ template class Loader : public LoaderBase { using RFutureWatcher = QFutureWatcher>; public: Loader(Support::Loader *loaderImpl, QObject *parent = nullptr) : Loader(nullptr, loaderImpl, parent) {} Loader(ApiClient *apiClient, Support::Loader *loaderImpl, QObject *parent = nullptr) : LoaderBase(apiClient, parent), m_loader(loaderImpl), m_futureWatcher(new QFutureWatcher>(this)) { m_dataViewModel = new T(this); connect(m_futureWatcher, &RFutureWatcher::finished, this, &Loader::updateData); } T *dataViewModel() const { return m_dataViewModel; } QObject *data() const { return m_dataViewModel; } void reload() override { if (m_futureWatcher->isRunning()) return; setStatus(Loading); this->m_loader->setApiClient(m_apiClient); m_loader->setParameters(m_parameters); m_loader->prepareLoad(); QFuture> future = QtConcurrent::run(this, &Loader::invokeLoader); m_futureWatcher->setFuture(future); } protected: T* m_dataViewModel; P m_parameters; /** * @brief Subclasses should initialize this to a loader that actually loads stuff. */ QScopedPointer> m_loader = nullptr; private: QFutureWatcher> *m_futureWatcher; /** * @brief Callback for QtConcurrent::run() * @param self Pointer to this class * @param parameters Parameters to forward to the loader * @return empty optional if an error occured, otherwise the result. */ std::optional invokeLoader() { QMutexLocker(&this->m_mutex); try { return this->m_loader->load(); } catch (Support::LoadException &e) { qWarning() << "Exception while loading an item: " << e.what(); this->setErrorString(QString(e.what())); return std::nullopt; } } /** * @brief Updates the data when finished. */ void updateData() { std::optional newDataOpt = m_futureWatcher->result(); if (newDataOpt.has_value()) { R newData = newDataOpt.value(); if (m_dataViewModel->data()->sameAs(newData)) { // Replace the data the model holds m_dataViewModel->data()->replaceData(newData); } else { // Replace the model using PointerType = typename decltype(m_dataViewModel->data())::Type; m_dataViewModel = new T(this, QSharedPointer::create(newData, m_apiClient)); } setStatus(Ready); emitDataChanged(); } else { setStatus(Error); } } QMutex m_mutex; }; void registerRemoteTypes(const char *uri); } // NS ViewModel } // NS Jellyfin #endif // JELLYFIN_DTO_DTO_H