1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-05-21 05:02:41 +00:00

WIP: Slowly bringing back viewmodels

This commit is contained in:
Chris Josten 2021-03-29 23:48:16 +02:00
parent 9abee12658
commit 228f81984b
17 changed files with 292 additions and 96 deletions

View file

@ -168,7 +168,7 @@ public:
m_startIndex = 0; m_startIndex = 0;
m_totalRecordCount = -1; m_totalRecordCount = -1;
emitModelShouldClear(); emitModelShouldClear();
loadMore(0, -1, ViewModel::ModelStatus::Loading); loadMore(0, m_limit, ViewModel::ModelStatus::Loading);
} }
void loadMore() { void loadMore() {
@ -272,7 +272,9 @@ protected:
// meaning loadMore is not supported. // meaning loadMore is not supported.
return; return;
} }
setRequestLimit<P>(this->m_parameters, limit); if (limit > 0) {
setRequestLimit<P>(this->m_parameters, limit);
}
this->setStatus(suggestedModelStatus); 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

View file

@ -30,6 +30,7 @@
#include "apiclient.h" #include "apiclient.h"
#include "apimodel.h" #include "apimodel.h"
#include "serverdiscoverymodel.h" #include "serverdiscoverymodel.h"
#include "websocket.h"
#include "viewmodel/item.h" #include "viewmodel/item.h"
#include "viewmodel/itemmodel.h" #include "viewmodel/itemmodel.h"
#include "viewmodel/loader.h" #include "viewmodel/loader.h"

View file

@ -33,6 +33,16 @@ namespace Model {
class Item : public DTO::BaseItemDto { class Item : public DTO::BaseItemDto {
public: public:
/**
* @brief Constructor that creates an empty item.
*/
Item();
/**
* @brief Copies the data from the DTO into this model and attaches an ApiClient
* @param data The DTO to copy information from
* @param apiClient The ApiClient to attach to, to listen for updates and so on.
*/
Item(const DTO::BaseItemDto &data, ApiClient *apiClient = nullptr); Item(const DTO::BaseItemDto &data, ApiClient *apiClient = nullptr);
virtual ~Item(); virtual ~Item();

View file

@ -152,9 +152,24 @@ QJsonValue toJsonValue(const QSharedPointer<T> &source, convertType<QSharedPoint
/** /**
* Templates for string conversion. * Templates for string conversion.
*/ */
template <typename T>
QString toString(const T &source, convertType<T>) {
return toJsonValue(source).toString();
}
template <typename T>
QString toString(const std::optional<T> &source, convertType<std::optional<T>>) {
if (source.has_value()) {
return toString<T>(source.value(), convertType<T>{});
} else {
return QString();
}
}
template <typename T> template <typename T>
QString toString(const T &source) { QString toString(const T &source) {
return toJsonValue(source).toString(); return toString(source, convertType<T>{});
} }

View file

@ -51,9 +51,7 @@ namespace ViewModel {
class Item : public QObject { class Item : public QObject {
Q_OBJECT Q_OBJECT
public: public:
explicit Item(QObject *parent = nullptr); explicit Item(QObject *parent = nullptr, QSharedPointer<Model::Item> data = QSharedPointer<Model::Item>::create());
explicit Item(QSharedPointer<Model::Item> data = QSharedPointer<Model::Item>(),
QObject *parent = nullptr);
// Please keep the order of the properties the same as in the file linked above. // Please keep the order of the properties the same as in the file linked above.
Q_PROPERTY(QUuid jellyfinId READ jellyfinId NOTIFY jellyfinIdChanged) Q_PROPERTY(QUuid jellyfinId READ jellyfinId NOTIFY jellyfinIdChanged)
@ -194,7 +192,7 @@ public:
Q_PROPERTY(QString itemId READ itemId WRITE setItemId NOTIFY itemIdChanged) Q_PROPERTY(QString itemId READ itemId WRITE setItemId NOTIFY itemIdChanged)
QString itemId() const { return m_parameters.itemId(); } QString itemId() const { return m_parameters.itemId(); }
void setItemId(QString newItemId) { m_parameters.setItemId(newItemId); } void setItemId(QString newItemId) { m_parameters.setItemId(newItemId); emit itemIdChanged(newItemId); }
virtual bool canReload() const override; virtual bool canReload() const override;
signals: signals:

View file

@ -44,7 +44,44 @@ namespace ViewModel {
// This file contains all models that expose a Model::Item // This file contains all models that expose a Model::Item
using UserViewsLoaderBase = LoaderModelLoader<Model::Item, DTO::BaseItemDto, DTO::BaseItemDtoQueryResult, Jellyfin::Loader::GetUserViewsParams>; /**
* @brief Class intended for models which have a mandatory userId property, which can be extracted from the
* ApiClient.
*/
template <class T, class D, class R, class P>
class AbstractUserParameterLoader : public LoaderModelLoader<T, D, R, P> {
public:
explicit AbstractUserParameterLoader(Support::Loader<R, P> *loader, QObject *parent = nullptr)
: LoaderModelLoader<T, D, R, P>(loader, parent) {
this->connect(this, &BaseModelLoader::apiClientChanged, this, &AbstractUserParameterLoader<T, D, R, P>::apiClientChanged);
}
protected:
virtual bool canReload() const override {
return BaseModelLoader::canReload() && !this->m_parameters.userId().isNull();
}
private:
void apiClientChanged(ApiClient *newApiClient) {
if (this->m_apiClient != nullptr) {
this->disconnect(this->m_apiClient, &ApiClient::userIdChanged, this, &AbstractUserParameterLoader<T, D, R, P>::userIdChanged);
}
if (newApiClient != nullptr) {
this->connect(newApiClient, &ApiClient::userIdChanged, this, &AbstractUserParameterLoader<T, D, R, P>::userIdChanged);
if (!newApiClient->userId().isNull()) {
this->m_parameters.setUserId(newApiClient->userId());
}
}
}
void userIdChanged(const QString &newUserId) {
this->m_parameters.setUserId(newUserId);
this->autoReloadIfNeeded();
}
};
/**
* Loads the views of an user, such as "Videos", "Music" and so on.
*/
using UserViewsLoaderBase = AbstractUserParameterLoader<Model::Item, DTO::BaseItemDto, DTO::BaseItemDtoQueryResult, Jellyfin::Loader::GetUserViewsParams>;
class UserViewsLoader : public UserViewsLoaderBase { class UserViewsLoader : public UserViewsLoaderBase {
Q_OBJECT Q_OBJECT
public: public:
@ -53,12 +90,27 @@ public:
FWDPROP(bool, includeExternalContent, IncludeExternalContent) FWDPROP(bool, includeExternalContent, IncludeExternalContent)
FWDPROP(bool, includeHidden, IncludeHidden) FWDPROP(bool, includeHidden, IncludeHidden)
FWDPROP(QStringList, presetViews, PresetViews) FWDPROP(QStringList, presetViews, PresetViews)
private slots:
void apiClientChanged(ApiClient *newApiClient);
void userIdChanged(const QString &newUserId);
}; };
using UserItemsLoaderBase = LoaderModelLoader<Model::Item, DTO::BaseItemDto, DTO::BaseItemDtoQueryResult, Jellyfin::Loader::GetItemsByUserIdParams>; using LatestMediaBase = AbstractUserParameterLoader<Model::Item, DTO::BaseItemDto, QList<DTO::BaseItemDto>, Jellyfin::Loader::GetLatestMediaParams>;
class LatestMediaLoader : public LatestMediaBase {
Q_OBJECT
public:
explicit LatestMediaLoader(QObject *parent = nullptr);
// Optional
FWDPROP(QList<Jellyfin::DTO::ImageTypeClass::Value>, enableImageTypes, EnableImageTypes)
FWDPROP(bool, enableImages, EnableImages)
FWDPROP(bool, enableUserData, EnableUserData)
FWDPROP(QList<Jellyfin::DTO::ItemFieldsClass::Value>, fields, Fields)
FWDPROP(bool, groupItems, GroupItems)
FWDPROP(qint32, imageTypeLimit, ImageTypeLimit)
FWDPROP(QStringList, includeItemTypes, IncludeItemTypes)
FWDPROP(bool, isPlayed, IsPlayed)
FWDPROP(QString, parentId, ParentId)
};
using UserItemsLoaderBase = AbstractUserParameterLoader<Model::Item, DTO::BaseItemDto, DTO::BaseItemDtoQueryResult, Jellyfin::Loader::GetItemsByUserIdParams>;
class UserItemsLoader : public UserItemsLoaderBase { class UserItemsLoader : public UserItemsLoaderBase {
Q_OBJECT Q_OBJECT
public: public:
@ -70,14 +122,26 @@ public:
FWDPROP(QStringList, albums, Albums) FWDPROP(QStringList, albums, Albums)
FWDPROP(QStringList, artistIds, ArtistIds) FWDPROP(QStringList, artistIds, ArtistIds)
FWDPROP(QStringList, artists, Artists) FWDPROP(QStringList, artists, Artists)
FWDPROP(bool, collapseBoxSetItems, CollapseBoxSetItems)
FWDPROP(QStringList, contributingArtistIds, ContributingArtistIds)
FWDPROP(QList<Jellyfin::DTO::ImageTypeClass::Value>, enableImageTypes, EnableImageTypes);
FWDPROP(bool, enableImages, EnableImages)
FWDPROP(bool, enableTotalRecordCount, EnableTotalRecordCount)
FWDPROP(bool, enableUserData, EnableUserData)
FWDPROP(QStringList, excludeArtistIds, ExcludeArtistIds)
FWDPROP(QStringList, excludeItemIds, ExcludeItemIds)
FWDPROP(QStringList, excludeItemTypes, ExcludeItemTypes)
FWDPROP(QList<Jellyfin::DTO::LocationTypeClass::Value>, excludeLocationTypes, ExcludeLocationTypes)
FWDPROP(QList<Jellyfin::DTO::ItemFieldsClass::Value>, fields, Fields)
FWDPROP(QList<Jellyfin::DTO::ItemFilterClass::Value>, filters, Filters)
FWDPROP(QString, parentId, ParentId)
FWDPROP(bool, recursive, Recursive) FWDPROP(bool, recursive, Recursive)
//FWDPROP(bool, collapseBoxSetItems) //FWDPROP(bool, collapseBoxSetItems)
protected:
virtual bool canReload() const override;
private slots:
void apiClientChanged(ApiClient *newApiClient);
void userIdChanged(const QString &newUserId);
}; };
/** /**
* @brief Base class for each model that works with items. * @brief Base class for each model that works with items.
*/ */
@ -95,7 +159,10 @@ public:
playlistItemId, playlistItemId,
dateCreated, dateCreated,
dateLastMediaAdded, dateLastMediaAdded,
extraType extraType,
// Hand-picked, important ones
imageTags
}; };
explicit ItemModel (QObject *parent = nullptr); explicit ItemModel (QObject *parent = nullptr);
@ -111,7 +178,9 @@ public:
JFRN(playlistItemId), JFRN(playlistItemId),
JFRN(dateCreated), JFRN(dateCreated),
JFRN(dateLastMediaAdded), JFRN(dateLastMediaAdded),
JFRN(extraType) JFRN(extraType),
// Handpicked, important ones
JFRN(imageTags),
}; };
} }
QVariant data(const QModelIndex &index, int role) const override; QVariant data(const QModelIndex &index, int role) const override;

View file

@ -144,13 +144,14 @@ template <class T, class R, class P>
class Loader : public LoaderBase { class Loader : public LoaderBase {
using RFutureWatcher = QFutureWatcher<std::optional<R>>; using RFutureWatcher = QFutureWatcher<std::optional<R>>;
public: public:
Loader(Support::Loader<R, P> loaderImpl, QObject *parent = nullptr) Loader(Support::Loader<R, P> *loaderImpl, QObject *parent = nullptr)
: Loader(nullptr, loaderImpl, parent) {} : Loader(nullptr, loaderImpl, parent) {}
Loader(ApiClient *apiClient, Support::Loader<R, P> loaderImpl, QObject *parent = nullptr) Loader(ApiClient *apiClient, Support::Loader<R, P> *loaderImpl, QObject *parent = nullptr)
: LoaderBase(apiClient, parent), : LoaderBase(apiClient, parent),
m_loader(loaderImpl), m_loader(loaderImpl),
m_futureWatcher(new QFutureWatcher<std::optional<R>>) { m_futureWatcher(new QFutureWatcher<std::optional<R>>(this)) {
m_dataViewModel = new T(this); m_dataViewModel = new T(this);
connect(m_futureWatcher, &RFutureWatcher::finished, this, &Loader<T, R, P>::updateData); connect(m_futureWatcher, &RFutureWatcher::finished, this, &Loader<T, R, P>::updateData);
} }
@ -161,8 +162,9 @@ public:
void reload() override { void reload() override {
if (m_futureWatcher->isRunning()) return; if (m_futureWatcher->isRunning()) return;
setStatus(Loading); setStatus(Loading);
m_loader.setParameters(m_parameters); this->m_loader->setApiClient(m_apiClient);
m_loader.prepareLoad(); m_loader->setParameters(m_parameters);
m_loader->prepareLoad();
QFuture<std::optional<R>> future = QtConcurrent::run(this, &Loader<T, R, P>::invokeLoader); QFuture<std::optional<R>> future = QtConcurrent::run(this, &Loader<T, R, P>::invokeLoader);
m_futureWatcher->setFuture(future); m_futureWatcher->setFuture(future);
} }
@ -172,7 +174,7 @@ protected:
/** /**
* @brief Subclasses should initialize this to a loader that actually loads stuff. * @brief Subclasses should initialize this to a loader that actually loads stuff.
*/ */
Support::Loader<R, P> m_loader; QScopedPointer<Support::Loader<R, P>> m_loader = nullptr;
private: private:
QFutureWatcher<std::optional<R>> *m_futureWatcher; QFutureWatcher<std::optional<R>> *m_futureWatcher;
@ -184,9 +186,8 @@ private:
*/ */
std::optional<R> invokeLoader() { std::optional<R> invokeLoader() {
QMutexLocker(&this->m_mutex); QMutexLocker(&this->m_mutex);
this->m_loader.setApiClient(m_apiClient);
try { try {
return this->m_loader.load(); return this->m_loader->load();
} catch (Support::LoadException &e) { } catch (Support::LoadException &e) {
qWarning() << "Exception while loading an item: " << e.what(); qWarning() << "Exception while loading an item: " << e.what();
this->setErrorString(QString(e.what())); this->setErrorString(QString(e.what()));
@ -206,7 +207,7 @@ private:
} else { } else {
// Replace the model // Replace the model
using PointerType = typename decltype(m_dataViewModel->data())::Type; using PointerType = typename decltype(m_dataViewModel->data())::Type;
m_dataViewModel = new T(QSharedPointer<PointerType>::create(newData), this); m_dataViewModel = new T(this, QSharedPointer<PointerType>::create(newData, m_apiClient));
} }
setStatus(Ready); setStatus(Ready);
emitDataChanged(); emitDataChanged();

View file

@ -80,7 +80,7 @@ public:
Q_PROPERTY(PlayMethod playMethod READ playMethod NOTIFY playMethodChanged) Q_PROPERTY(PlayMethod playMethod READ playMethod NOTIFY playMethodChanged)
// Current Item and queue informatoion // Current Item and queue informatoion
Q_PROPERTY(Model::Item *item READ item NOTIFY itemChanged) Q_PROPERTY(ViewModel::Item *item READ item NOTIFY itemChanged)
Q_PROPERTY(QAbstractItemModel *queue READ queue NOTIFY queueChanged) Q_PROPERTY(QAbstractItemModel *queue READ queue NOTIFY queueChanged)
Q_PROPERTY(int queueIndex READ queueIndex NOTIFY queueIndexChanged) Q_PROPERTY(int queueIndex READ queueIndex NOTIFY queueIndexChanged)
@ -94,7 +94,7 @@ public:
Q_PROPERTY(QMediaPlayer::State playbackState READ playbackState NOTIFY playbackStateChanged) Q_PROPERTY(QMediaPlayer::State playbackState READ playbackState NOTIFY playbackStateChanged)
Q_PROPERTY(qint64 position READ position NOTIFY positionChanged) Q_PROPERTY(qint64 position READ position NOTIFY positionChanged)
Model::Item *item() const { return m_item.data(); } ViewModel::Item *item() const { return m_displayItem.get(); }
void setApiClient(ApiClient *apiClient); void setApiClient(ApiClient *apiClient);
QString streamUrl() const { return m_streamUrl; } QString streamUrl() const { return m_streamUrl; }
@ -169,6 +169,8 @@ private:
QTimer m_updateTimer; QTimer m_updateTimer;
ApiClient *m_apiClient = nullptr; ApiClient *m_apiClient = nullptr;
QSharedPointer<Model::Item> m_item; QSharedPointer<Model::Item> m_item;
QScopedPointer<ViewModel::Item> m_displayItem = QScopedPointer<ViewModel::Item>(new ViewModel::Item());
QString m_streamUrl; QString m_streamUrl;
QString m_playSessionId; QString m_playSessionId;
int m_audioIndex = 0; int m_audioIndex = 0;

View file

@ -62,7 +62,7 @@ void BaseModelLoader::setApiClient(ApiClient *newApiClient) {
void BaseModelLoader::setLimit(int newLimit) { void BaseModelLoader::setLimit(int newLimit) {
int oldLimit = this->m_limit; int oldLimit = this->m_limit;
m_limit = newLimit; m_limit = newLimit;
if (oldLimit != this->m_limit) { if (oldLimit != newLimit) {
emit limitChanged(this->m_limit); emit limitChanged(this->m_limit);
} }
} }
@ -76,6 +76,7 @@ void BaseModelLoader::setAutoReload(bool newAutoReload) {
bool BaseModelLoader::canReload() const { bool BaseModelLoader::canReload() const {
return m_apiClient != nullptr return m_apiClient != nullptr
&& !m_isBeingParsed
// If the loader for this model needs authentication (almost every one does) // If the loader for this model needs authentication (almost every one does)
// block if the ApiClient is not authenticated yet. // block if the ApiClient is not authenticated yet.
&& (!m_needsAuthentication || m_apiClient->authenticated()) && (!m_needsAuthentication || m_apiClient->authenticated())
@ -88,6 +89,8 @@ void BaseApiModel::reload() {
qWarning() << " BaseApiModel slot called instead of overloaded method"; qWarning() << " BaseApiModel slot called instead of overloaded method";
} }
// Parameters injectors and result extractors
template <> template <>
bool setRequestStartIndex(Loader::GetUserViewsParams &params, int startIndex) { bool setRequestStartIndex(Loader::GetUserViewsParams &params, int startIndex) {
// Not supported // Not supported
@ -112,6 +115,28 @@ int extractTotalRecordCount(const DTO::BaseItemDtoQueryResult &result) {
return result.totalRecordCount(); return result.totalRecordCount();
} }
template <>
QList<DTO::BaseItemDto> extractRecords(const QList<DTO::BaseItemDto> &result) {
return result;
}
template <>
int extractTotalRecordCount(const QList<DTO::BaseItemDto> &result) {
return result.size();
}
template<>
void setRequestLimit(Loader::GetLatestMediaParams &params, int limit) {
params.setLimit(limit);
}
template<>
bool setRequestStartIndex(Loader::GetLatestMediaParams &params, int offset) {
Q_UNUSED(params)
Q_UNUSED(offset)
return false;
}
void registerModels(const char *URI) { void registerModels(const char *URI) {
Q_UNUSED(URI) Q_UNUSED(URI)

View file

@ -22,20 +22,25 @@ namespace Jellyfin {
void registerTypes(const char *uri) { void registerTypes(const char *uri) {
qmlRegisterType<ApiClient>(uri, 1, 0, "ApiClient"); qmlRegisterType<ApiClient>(uri, 1, 0, "ApiClient");
qmlRegisterType<ServerDiscoveryModel>(uri, 1, 0, "ServerDiscoveryModel"); qmlRegisterType<ServerDiscoveryModel>(uri, 1, 0, "ServerDiscoveryModel");
qmlRegisterType<ViewModel::PlaybackManager>(uri, 1, 0, "PlaybackManager");
qmlRegisterUncreatableType<ViewModel::Item>(uri, 1, 0, "Item", "Acquire one via ItemLoader or exposed properties");
qmlRegisterUncreatableType<WebSocket>(uri, 1, 0, "WebSocket", "Obtain one via your ApiClient");
// AbstractItemModels
qmlRegisterUncreatableType<BaseApiModel>(uri, 1, 0, "BaseApiModel", "Please use one of its subclasses"); qmlRegisterUncreatableType<BaseApiModel>(uri, 1, 0, "BaseApiModel", "Please use one of its subclasses");
qmlRegisterUncreatableType<BaseModelLoader>(uri, 1, 0, "BaseModelLoader", "Please use one of its subclasses"); qmlRegisterUncreatableType<BaseModelLoader>(uri, 1, 0, "BaseModelLoader", "Please use one of 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");
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::PlaybackManager>(uri, 1, 0, "PlaybackManager"); // Loaders
qmlRegisterUncreatableType<ViewModel::LoaderBase>(uri, 1, 0, "LoaderBase", "Use one of its subclasses");
qmlRegisterType<ViewModel::ItemLoader>(uri, 1, 0, "ItemLoader");
qmlRegisterType<ViewModel::LatestMediaLoader>(uri, 1, 0, "LatestMediaLoader");
qmlRegisterType<ViewModel::UserItemsLoader>(uri, 1, 0, "UserItemsLoader");
qmlRegisterType<ViewModel::UserViewsLoader>(uri, 1, 0, "UsersViewsLoader");
// Enumerations
qmlRegisterUncreatableType<DTO::GeneralCommandTypeClass>(uri, 1, 0, "GeneralCommandType", "Is an enum"); qmlRegisterUncreatableType<DTO::GeneralCommandTypeClass>(uri, 1, 0, "GeneralCommandType", "Is an enum");
qmlRegisterUncreatableType<ViewModel::ModelStatusClass>(uri, 1, 0, "ModelStatus", "Is an enum"); qmlRegisterUncreatableType<ViewModel::ModelStatusClass>(uri, 1, 0, "ModelStatus", "Is an enum");
} }
} }

View file

@ -22,6 +22,10 @@
namespace Jellyfin { namespace Jellyfin {
namespace Model { namespace Model {
Item::Item()
: Item(DTO::BaseItemDto(), nullptr) { }
Item::Item(const DTO::BaseItemDto &data, ApiClient *apiClient) Item::Item(const DTO::BaseItemDto &data, ApiClient *apiClient)
: DTO::BaseItemDto(data), m_apiClient(apiClient) { : DTO::BaseItemDto(data), m_apiClient(apiClient) {
if (m_apiClient != nullptr) { if (m_apiClient != nullptr) {

View file

@ -149,8 +149,14 @@ QJsonValue toJsonValue<QStringList>(const QStringList &source, convertType<QStri
// QJsonObject // QJsonObject
template <> template <>
QJsonObject fromJsonValue<QJsonObject>(const QJsonValue &source, convertType<QJsonObject>) { QJsonObject fromJsonValue<QJsonObject>(const QJsonValue &source, convertType<QJsonObject>) {
if (!source.isObject()) throw ParseException("Error parsing JSON value as object: not a double"); switch(source.type()) {
return source.toObject(); case QJsonValue::Null:
return QJsonObject();
case QJsonValue::Object:
return source.toObject();
default:
throw ParseException("Error parsing JSON value as object: not an object");
}
} }
template <> template <>
@ -161,7 +167,7 @@ QJsonValue toJsonValue<QJsonObject>(const QJsonObject &source, convertType<QJson
// Double // Double
template <> template <>
double fromJsonValue<double>(const QJsonValue &source, convertType<double>) { double fromJsonValue<double>(const QJsonValue &source, convertType<double>) {
if (!source.isDouble()) throw ParseException("Error parsing JSON value as integer: not a double"); if (!source.isDouble()) throw ParseException("Error parsing JSON value as double: not a double");
return source.toDouble(); return source.toDouble();
} }
@ -170,6 +176,18 @@ QJsonValue toJsonValue<double>(const double &source, convertType<double>) {
return QJsonValue(source); return QJsonValue(source);
} }
// Float
template <>
float fromJsonValue<float>(const QJsonValue &source, convertType<float>) {
if (!source.isDouble()) throw ParseException("Error parsing JSON value as float: not a double");
return static_cast<float>(source.toDouble());
}
template <>
QJsonValue toJsonValue<float>(const float &source, convertType<float>) {
return QJsonValue(static_cast<double>(source));
}
// QDateTime // QDateTime
template <> template <>
QDateTime fromJsonValue<QDateTime>(const QJsonValue &source, convertType<QDateTime>) { QDateTime fromJsonValue<QDateTime>(const QJsonValue &source, convertType<QDateTime>) {
@ -205,27 +223,37 @@ QJsonValue toJsonValue<QUuid>(const QUuid &source, convertType<QUuid>) {
// String types // String types
template <> template <>
QString toString(const QUuid &source) { QString toString(const QUuid &source, convertType<QUuid>) {
return uuidToString(source); return uuidToString(source);
} }
template <> template <>
QString toString(const qint32 &source) { QString toString(const qint32 &source, convertType<qint32>) {
return QString::number(source); return QString::number(source);
} }
template <> template <>
QString toString(const qint64 &source) { QString toString(const qint64 &source, convertType<qint64>) {
return QString::number(source); return QString::number(source);
} }
template <> template <>
QString toString(const bool &source) { QString toString(const float &source, convertType<float>) {
return QString::number(source);
}
template <>
QString toString(const double &source, convertType<double>) {
return QString::number(source);
}
template <>
QString toString(const bool &source, convertType<bool>) {
return source ? QStringLiteral("true") : QStringLiteral("false"); return source ? QStringLiteral("true") : QStringLiteral("false");
} }
template <> template <>
QString toString(const QString &source) { QString toString(const QString &source, convertType<QString>) {
return source; return source;
} }

View file

@ -21,11 +21,10 @@
namespace Jellyfin { namespace Jellyfin {
namespace ViewModel { namespace ViewModel {
Item::Item(QObject *parent) Item::Item(QObject *parent, QSharedPointer<Model::Item> data)
: Item(nullptr, parent){} : QObject(parent), m_data(data){
Item::Item(QSharedPointer<Model::Item> data, QObject *parent) }
: QObject(parent), m_data(data){}
void Item::setData(QSharedPointer<Model::Item> newData) { void Item::setData(QSharedPointer<Model::Item> newData) {
Model::Item oldData = *m_data.data(); Model::Item oldData = *m_data.data();
@ -36,7 +35,8 @@ void Item::setData(QSharedPointer<Model::Item> newData) {
// ItemLoader // ItemLoader
ItemLoader::ItemLoader(QObject *parent) ItemLoader::ItemLoader(QObject *parent)
: BaseClass(Jellyfin::Loader::HTTP::GetItemLoader(), parent) { : BaseClass(new Jellyfin::Loader::HTTP::GetItemLoader(), parent) {
connect(this, &LoaderBase::apiClientChanged, this, &ItemLoader::onApiClientChanged);
} }
void ItemLoader::onApiClientChanged(ApiClient *newApiClient) { void ItemLoader::onApiClientChanged(ApiClient *newApiClient) {
@ -54,7 +54,9 @@ void ItemLoader::setUserId(const QString &newUserId) {
} }
bool ItemLoader::canReload() const { bool ItemLoader::canReload() const {
return BaseClass::canReload() && !m_parameters.itemId().isEmpty(); return BaseClass::canReload()
&& !m_parameters.itemId().isEmpty()
&& !m_parameters.userId().isEmpty();
} }
} }

View file

@ -18,6 +18,9 @@
*/ */
#include "JellyfinQt/viewmodel/itemmodel.h" #include "JellyfinQt/viewmodel/itemmodel.h"
#include "JellyfinQt/loader/http/getlatestmedia.h"
#include "JellyfinQt/loader/http/getitemsbyuserid.h"
#define JF_CASE(roleName) case roleName: \ #define JF_CASE(roleName) case roleName: \
try { \ try { \
return QVariant(item.roleName()); \ return QVariant(item.roleName()); \
@ -30,37 +33,13 @@ namespace Jellyfin {
namespace ViewModel { namespace ViewModel {
UserViewsLoader::UserViewsLoader(QObject *parent) UserViewsLoader::UserViewsLoader(QObject *parent)
: UserViewsLoaderBase(new Jellyfin::Loader::HTTP::GetUserViewsLoader(), parent) { : UserViewsLoaderBase(new Jellyfin::Loader::HTTP::GetUserViewsLoader(), parent) { }
connect(this, &BaseModelLoader::apiClientChanged, this, &UserViewsLoader::apiClientChanged);
}
void UserViewsLoader::apiClientChanged(ApiClient *newApiClient) { LatestMediaLoader::LatestMediaLoader(QObject *parent)
if (m_apiClient != nullptr) disconnect(m_apiClient, &ApiClient::userIdChanged, this, &UserViewsLoader::userIdChanged); : LatestMediaBase(new Jellyfin::Loader::HTTP::GetLatestMediaLoader(), parent){ }
if (newApiClient != nullptr) {
connect(newApiClient, &ApiClient::userIdChanged, this, &UserViewsLoader::userIdChanged);
if (!newApiClient->userId().isNull()) {
m_parameters.setUserId(newApiClient->userId());
}
}
}
void UserViewsLoader::userIdChanged(const QString &newUserId) { UserItemsLoader::UserItemsLoader(QObject *parent)
m_parameters.setUserId(newUserId); : UserItemsLoaderBase(new Jellyfin::Loader::HTTP::GetItemsByUserIdLoader(), parent) {}
autoReloadIfNeeded();
}
void UserItemsLoader::apiClientChanged(ApiClient *newApiClient) {
if (m_apiClient != nullptr) disconnect(m_apiClient, &ApiClient::userIdChanged, this, &UserItemsLoader::userIdChanged);
if (newApiClient != nullptr) connect(newApiClient, &ApiClient::userIdChanged, this, &UserItemsLoader::userIdChanged);
}
void UserItemsLoader::userIdChanged(const QString &newUserId) {
m_parameters.setUserId(newUserId);
autoReloadIfNeeded();
}
bool UserItemsLoader::canReload() const {
return BaseModelLoader::canReload() && !m_parameters.userId().isNull();
}
ItemModel::ItemModel(QObject *parent) ItemModel::ItemModel(QObject *parent)
: ApiModel<Model::Item>(parent) { } : ApiModel<Model::Item>(parent) { }
@ -81,6 +60,8 @@ QVariant ItemModel::data(const QModelIndex &index, int role) const {
JF_CASE(dateCreated) JF_CASE(dateCreated)
JF_CASE(dateLastMediaAdded) JF_CASE(dateLastMediaAdded)
JF_CASE(extraType) JF_CASE(extraType)
// Handpicked, important ones
JF_CASE(imageTags)
default: default:
return QVariant(); return QVariant();
} }

View file

@ -14,6 +14,11 @@ ApplicationWindow {
height: 600 height: 600
visible: true visible: true
property int _oldDepth: 0 property int _oldDepth: 0
property alias playbackManager: playbackManager
J.PlaybackManager {
id: playbackManager
}
background: Background { background: Background {
id: background id: background
@ -43,4 +48,20 @@ ApplicationWindow {
Component.onCompleted: { Component.onCompleted: {
ApiClient.restoreSavedSession() ApiClient.restoreSavedSession()
} }
footer: Column {
id: footer
Text {
text: qsTr("Now playing")
color: "white"
}
Text {
text: playbackManager.item.name ? playbackManager.item.name : "Nothing"
color: "white"
}
}
Rectangle {
color: "darkblue"
anchors.fill: footer
}
} }

View file

@ -6,12 +6,14 @@ import nl.netsoj.chris.Jellyfin 1.0 as J
import "../components" import "../components"
import "../.." import "../.."
import ".."
Page { Page {
id: detailPage
property bool _modelsLoaded: false property bool _modelsLoaded: false
property StackView stackView: StackView.view property StackView stackView: StackView.view
property string itemId property string itemId
property alias jellyfinItem: jellyfinItem.data property alias jellyfinItem: jellyfinItemLoader.data
header: ToolBar { header: ToolBar {
Label { Label {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@ -25,13 +27,33 @@ Page {
onClicked: stackView.pop() onClicked: stackView.pop()
} }
J.ItemLoader { J.ItemLoader {
id: jellyfinItem id: jellyfinItemLoader
jellyfinId: itemId itemId: detailPage.itemId
apiClient: ApiClient apiClient: ApiClient
} }
Image { Image {
anchors.centerIn: parent anchors.top: parent.top
width: parent.width
height: parent.height / 3
source: ApiClient.baseUrl + "/Items/" + itemId + "/Images/Primary?tag=" + jellyfinItem.tag source: ApiClient.baseUrl + "/Items/" + itemId + "/Images/Primary?tag=" + jellyfinItem.tag
} }
ListView {
width: parent.width
height: parent.height / 3 * 2
anchors.bottom: parent.bottom
model: J.ItemModel {
loader: J.UserItemsLoader {
apiClient: ApiClient
parentId: detailPage.itemId
}
}
delegate: ItemDelegate{
icon.source: ApiClient.baseUrl + "/Items/" + model.jellyfinId + "/Images/Primary?tag=" + model.tag
text: model.name
width: parent.width
onClicked: playbackManager.play(model.jellyfinId)
}
}
} }

View file

@ -21,7 +21,7 @@ Page {
J.ItemModel { J.ItemModel {
id: mediaLibraryModel id: mediaLibraryModel
loader: J.UsersViewLoader { loader: J.UsersViewsLoader {
id: mediaLibraryModelLoader id: mediaLibraryModelLoader
apiClient: ApiClient apiClient: ApiClient
} }
@ -30,6 +30,7 @@ Page {
ScrollView { ScrollView {
anchors.fill: parent anchors.fill: parent
contentHeight: content.height contentHeight: content.height
contentWidth: availableWidth
Column { Column {
id: content id: content
width: parent.width width: parent.width
@ -37,12 +38,15 @@ Page {
model: mediaLibraryModel model: mediaLibraryModel
Column { Column {
width: parent.width width: parent.width
/*J.UserItemLatestModel { J.ItemModel{
id: userItemModel id: userItemModel
apiClient: ApiClient loader: J.LatestMediaLoader {
parentId: model.id id: latestMediaLoader
limit: 16 apiClient: ApiClient
}*/ parentId: model.jellyfinId
//limit: 16
}
}
Label { Label {
text: model.name ? model.name : "<Model without name>" text: model.name ? model.name : "<Model without name>"
} }
@ -51,13 +55,14 @@ Page {
width: parent.width width: parent.width
height: SailfinStyle.unit * 20 height: SailfinStyle.unit * 20
orientation: ListView.Horizontal orientation: ListView.Horizontal
model: 10 // userItemModel model: userItemModel
delegate: ItemDelegate { delegate: ItemDelegate {
width: 12 * SailfinStyle.unit width: 12 * SailfinStyle.unit
height: 20 * SailfinStyle.unit height: 20 * SailfinStyle.unit
Image { Image {
anchors.fill: parent anchors.fill: parent
source: ApiClient.baseUrl + "/Items/" + model.id + "/Images/Primary?tag=" + model.tag source: ApiClient.baseUrl + "/Items/" + model.jellyfinId
+ "/Images/Primary?tag=" + model.imageTags["Primary"] //model.tag
} }
Label { Label {
anchors.left: parent.left anchors.left: parent.left
@ -65,14 +70,18 @@ Page {
anchors.right: parent.right anchors.right: parent.right
text: model.name text: model.name
} }
onClicked: stackView.push(Qt.resolvedUrl("DetailPage.qml"), {"itemId": model.id}) onClicked: stackView.push(Qt.resolvedUrl(
"DetailPage.qml"), {
"itemId": model.jellyfinId
})
} }
} }
Connections { Connections {
target: mediaLibraryModelLoader target: mediaLibraryModelLoader
onReady: { onReady: {
if (mediaLibraryModelLoader.status === ModelStatus.Ready) { if (mediaLibraryModelLoader.status === ModelStatus.Ready) {
//userItemModel.reload()
latestMediaLoader.reload()
} }
} }
} }
@ -81,13 +90,14 @@ Page {
} }
} }
/** /**
* Loads models if not laoded. Set force to true to reload models * Loads models if not laoded. Set force to true to reload models
* even if loaded. * even if loaded.
*/ */
function loadModels(force) { function loadModels(force) {
if (force || (ApiClient.authenticated && !_modelsLoaded)) { if (force || (ApiClient.authenticated && !_modelsLoaded)) {
_modelsLoaded = true; _modelsLoaded = true
mediaLibraryModel.reload() mediaLibraryModel.reload()
} }
} }