1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-05-05 22:12:42 +00:00

Fix a few bugs and unimplemented features

* Show the now playing cover when playing an item, otherwise show the
  collection cover.
* ItemModelLoaders now correctly expose list properties of non-built-in
  Qt objects
* toString is now implemented for lists, fixing some query
  construction code.
* PlaybackManager now clears the playlist when playing a single item to
  prevent weird behaviour.
* The covers are slightly updated.
This commit is contained in:
Henk Kalkwater 2021-09-09 05:57:41 +02:00 committed by Henk Kalkwater
parent 60bc90c5fa
commit caf72af999
No known key found for this signature in database
GPG key ID: A69C050E9FD9FF6A
19 changed files with 179 additions and 81 deletions

View file

@ -47,6 +47,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include "support/loader.h" #include "support/loader.h"
#include "viewmodel/modelstatus.h" #include "viewmodel/modelstatus.h"
Q_DECLARE_LOGGING_CATEGORY(jellyfinApiModel)
namespace Jellyfin { namespace Jellyfin {
/* /*
@ -121,6 +123,7 @@ protected:
int m_limit = -1; int m_limit = -1;
int m_startIndex = 0; int m_startIndex = 0;
int m_totalRecordCount = 0; int m_totalRecordCount = 0;
bool m_explicitLimitSet = false;
const int DEFAULT_LIMIT = 100; const int DEFAULT_LIMIT = 100;
void emitModelShouldClear() { emit modelShouldClear(); } void emitModelShouldClear() { emit modelShouldClear(); }
void emitItemsLoaded() { emit itemsLoaded(); } void emitItemsLoaded() { emit itemsLoaded(); }
@ -165,14 +168,14 @@ public:
m_startIndex = 0; m_startIndex = 0;
m_totalRecordCount = -1; m_totalRecordCount = -1;
emitModelShouldClear(); emitModelShouldClear();
loadMore(0, m_limit, ViewModel::ModelStatus::Loading); loadMore(ViewModel::ModelStatus::Loading);
} }
void loadMore() { void loadMore() {
if (!canReload()) { if (!canReload()) {
return; return;
} }
loadMore(m_startIndex, m_limit, ViewModel::ModelStatus::LoadingMore); loadMore(ViewModel::ModelStatus::LoadingMore);
} }
virtual bool canLoadMore() const { virtual bool canLoadMore() const {
@ -192,12 +195,10 @@ protected:
* The itemsLoaded() signal is emitted when new data is ready. Call * The itemsLoaded() signal is emitted when new data is ready. Call
* getLoadedItems to retrieve the loaded items. * getLoadedItems to retrieve the loaded items.
* *
* @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). * @param suggestedStatus The suggested status this model should take on if it is able to load (more).
* Either LOADING or LOAD_MORE. * Either LOADING or LOAD_MORE.
*/ */
virtual void loadMore(int offset, int limit, ViewModel::ModelStatus suggestedStatus) = 0; virtual void loadMore(ViewModel::ModelStatus suggestedStatus) = 0;
std::pair<QList<T*>, int> m_result; std::pair<QList<T*>, int> m_result;
}; };
@ -270,7 +271,7 @@ public:
this->connect(m_loader.data(), &Support::Loader<R, P>::error, this, &LoaderModelLoader<T, D, R, P>::loaderError); this->connect(m_loader.data(), &Support::Loader<R, P>::error, this, &LoaderModelLoader<T, D, R, P>::loaderError);
} }
protected: protected:
void loadMore(int offset, int limit, ViewModel::ModelStatus suggestedModelStatus) override { void loadMore(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_loader->isRunning()) { if (m_loader->isRunning()) {
@ -279,20 +280,22 @@ protected:
// 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) if (!setRequestStartIndex<P>(this->m_parameters, this->m_startIndex)
&& suggestedModelStatus == ViewModel::ModelStatus::LoadingMore) { && suggestedModelStatus == ViewModel::ModelStatus::LoadingMore) {
// This loader's parameters does not setting a starting index, // This loader's parameters does not setting a starting index,
// meaning loadMore is not supported. // meaning loadMore is not supported.
return; return;
} }
setRequestStartIndex<P>(this->m_parameters, this->m_startIndex);
if (limit > 0) { if (suggestedModelStatus == ViewModel::ModelStatus::LoadingMore && this->m_explicitLimitSet) {
if (suggestedModelStatus == ViewModel::ModelStatus::Loading) { // If an explicit limit is set, we should load no more
setRequestLimit<P>(this->m_parameters, limit); return;
} else { }
// If an explicit limit is set, we should load no more
return; qCDebug(jellyfinApiModel) << "Explicit limit set: " << this->m_explicitLimitSet << ", " << this->m_limit;
} if (this->m_explicitLimitSet) {
setRequestLimit<P>(this->m_parameters, this->m_limit);
} else { } else {
setRequestLimit<P>(this->m_parameters, this->DEFAULT_LIMIT); setRequestLimit<P>(this->m_parameters, this->DEFAULT_LIMIT);
} }

View file

@ -100,6 +100,12 @@ public:
*/ */
void appendToList(ViewModel::ItemModel &model); void appendToList(ViewModel::ItemModel &model);
/**
* @brief appendToList Appends a single item to the current list
* @param item The item to append
*/
void appendToList(QSharedPointer<Model::Item> item);
/** /**
* @brief Start playing this playlist * @brief Start playing this playlist
* @param index The index to start from. * @param index The index to start from.

View file

@ -139,6 +139,12 @@ QJsonValue toJsonValue(const QSharedPointer<T> &source, convertType<QSharedPoint
* Templates for string conversion. * Templates for string conversion.
*/ */
template <typename T>
QString toString(const T &source) {
return toString(source, convertType<T>{});
}
template <typename T> template <typename T>
QString toString(const T &source, convertType<T>) { QString toString(const T &source, convertType<T>) {
return toJsonValue(source).toString(); return toJsonValue(source).toString();
@ -154,11 +160,15 @@ QString toString(const std::optional<T> &source, convertType<std::optional<T>>)
} }
template <typename T> template <typename T>
QString toString(const T &source) { QString toString(const QList<T> &source, convertType<QList<T>>) {
return toString(source, convertType<T>{}); QStringList tmp;
tmp.reserve(source.size());
for (auto it = source.cbegin(); it != source.cend(); it++) {
tmp.append(toString<T>(*it, convertType<T>{}));
}
return tmp.join(',');
} }
} // NS Support } // NS Support
} // NS Jellyfin } // NS Jellyfin

View file

@ -22,6 +22,7 @@
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QObject> #include <QObject>
#include <QScopedPointer> #include <QScopedPointer>
#include <QVariantList>
#include "../dto/baseitemdto.h" #include "../dto/baseitemdto.h"
#include "../dto/baseitemdtoqueryresult.h" #include "../dto/baseitemdtoqueryresult.h"
@ -45,6 +46,31 @@
Q_SIGNALS: \ Q_SIGNALS: \
void propName##Changed(); void propName##Changed();
#define FWDLISTPROP(type, propName, propSetName) \
public: \
Q_PROPERTY(QVariantList propName READ propName WRITE set##propSetName NOTIFY propName##Changed) \
QVariantList propName() const { \
QVariantList result; \
QList<type> list; \
result.reserve(list.size()); \
for (auto it = list.cbegin(); it != list.cend(); it++) { \
result.append(QVariant::fromValue<type>(*it)); \
} \
return result; \
} \
void set##propSetName(const QVariantList &newValue) { \
QList<type> list;\
list.reserve(newValue.size()); \
for(auto it = newValue.cbegin(); it != newValue.cend(); it++) { \
list.append(it->value<type>()); \
} \
this->m_parameters.set##propSetName(list); \
emit propName##Changed(); \
autoReloadIfNeeded(); \
} \
Q_SIGNALS: \
void propName##Changed();
namespace Jellyfin { namespace Jellyfin {
namespace ViewModel { namespace ViewModel {
@ -112,7 +138,7 @@ public:
FWDPROP(QList<Jellyfin::DTO::ImageTypeClass::Value>, enableImageTypes, EnableImageTypes) FWDPROP(QList<Jellyfin::DTO::ImageTypeClass::Value>, enableImageTypes, EnableImageTypes)
FWDPROP(bool, enableImages, EnableImages) FWDPROP(bool, enableImages, EnableImages)
FWDPROP(bool, enableUserData, EnableUserData) FWDPROP(bool, enableUserData, EnableUserData)
FWDPROP(QList<Jellyfin::DTO::ItemFieldsClass::Value>, fields, Fields) FWDLISTPROP(Jellyfin::DTO::ItemFieldsClass::Value, fields, Fields)
FWDPROP(bool, groupItems, GroupItems) FWDPROP(bool, groupItems, GroupItems)
FWDPROP(qint32, imageTypeLimit, ImageTypeLimit) FWDPROP(qint32, imageTypeLimit, ImageTypeLimit)
FWDPROP(QStringList, includeItemTypes, IncludeItemTypes) FWDPROP(QStringList, includeItemTypes, IncludeItemTypes)
@ -134,7 +160,7 @@ public:
FWDPROP(QStringList, artists, Artists) FWDPROP(QStringList, artists, Artists)
FWDPROP(bool, collapseBoxSetItems, CollapseBoxSetItems) FWDPROP(bool, collapseBoxSetItems, CollapseBoxSetItems)
FWDPROP(QStringList, contributingArtistIds, ContributingArtistIds) FWDPROP(QStringList, contributingArtistIds, ContributingArtistIds)
FWDPROP(QList<Jellyfin::DTO::ImageTypeClass::Value>, enableImageTypes, EnableImageTypes); FWDLISTPROP(Jellyfin::DTO::ImageTypeClass::Value, enableImageTypes, EnableImageTypes);
FWDPROP(bool, enableImages, EnableImages) FWDPROP(bool, enableImages, EnableImages)
FWDPROP(bool, enableTotalRecordCount, EnableTotalRecordCount) FWDPROP(bool, enableTotalRecordCount, EnableTotalRecordCount)
FWDPROP(bool, enableUserData, EnableUserData) FWDPROP(bool, enableUserData, EnableUserData)
@ -142,8 +168,8 @@ public:
FWDPROP(QStringList, excludeItemIds, ExcludeItemIds) FWDPROP(QStringList, excludeItemIds, ExcludeItemIds)
FWDPROP(QStringList, excludeItemTypes, ExcludeItemTypes) FWDPROP(QStringList, excludeItemTypes, ExcludeItemTypes)
FWDPROP(QList<Jellyfin::DTO::LocationTypeClass::Value>, excludeLocationTypes, ExcludeLocationTypes) FWDPROP(QList<Jellyfin::DTO::LocationTypeClass::Value>, excludeLocationTypes, ExcludeLocationTypes)
FWDPROP(QList<Jellyfin::DTO::ItemFieldsClass::Value>, fields, Fields) FWDLISTPROP(Jellyfin::DTO::ItemFieldsClass::Value, fields, Fields)
FWDPROP(QList<Jellyfin::DTO::ItemFilterClass::Value>, filters, Filters) FWDLISTPROP(Jellyfin::DTO::ItemFilterClass::Value, filters, Filters)
FWDPROP(QStringList, genreIds, GenreIds) FWDPROP(QStringList, genreIds, GenreIds)
FWDPROP(QStringList, genres, Genres) FWDPROP(QStringList, genres, Genres)
FWDPROP(bool, hasImdbId, HasImdbId) FWDPROP(bool, hasImdbId, HasImdbId)
@ -159,7 +185,7 @@ public:
FWDPROP(bool, hasTvdbId, HasTvdbId) FWDPROP(bool, hasTvdbId, HasTvdbId)
FWDPROP(QStringList, ids, Ids) FWDPROP(QStringList, ids, Ids)
FWDPROP(qint32, imageTypeLimit, ImageTypeLimit) FWDPROP(qint32, imageTypeLimit, ImageTypeLimit)
FWDPROP(QList<Jellyfin::DTO::ImageTypeClass::Value>, imageTypes, ImageTypes) FWDLISTPROP(Jellyfin::DTO::ImageTypeClass::Value, imageTypes, ImageTypes)
FWDPROP(QStringList, includeItemTypes, IncludeItemTypes) FWDPROP(QStringList, includeItemTypes, IncludeItemTypes)
FWDPROP(bool, is3D, Is3D) FWDPROP(bool, is3D, Is3D)
FWDPROP(bool, is4K, Is4K) FWDPROP(bool, is4K, Is4K)
@ -170,8 +196,7 @@ public:
FWDPROP(bool, isPlaceHolder, IsPlaceHolder) FWDPROP(bool, isPlaceHolder, IsPlaceHolder)
FWDPROP(bool, isPlayed, IsPlayed) FWDPROP(bool, isPlayed, IsPlayed)
FWDPROP(bool, isUnaired, IsUnaired) FWDPROP(bool, isUnaired, IsUnaired)
FWDPROP(int, limit, Limit) FWDLISTPROP(Jellyfin::DTO::LocationTypeClass::Value, locationTypes, LocationTypes)
FWDPROP(QList<Jellyfin::DTO::LocationTypeClass::Value>, locationTypes, LocationTypes)
FWDPROP(qint32, maxHeight, MaxHeight) FWDPROP(qint32, maxHeight, MaxHeight)
FWDPROP(QString, maxOfficialRating, MaxOfficialRating) FWDPROP(QString, maxOfficialRating, MaxOfficialRating)
FWDPROP(QDateTime, maxPremiereDate, MaxPremiereDate) FWDPROP(QDateTime, maxPremiereDate, MaxPremiereDate)
@ -198,12 +223,12 @@ class ResumeItemsLoader : public ResumeItemsLoaderBase {
public: public:
explicit ResumeItemsLoader(QObject *parent = nullptr); explicit ResumeItemsLoader(QObject *parent = nullptr);
FWDPROP(QList<Jellyfin::DTO::ImageTypeClass::Value>, enableImageTypes, EnableImageTypes); FWDLISTPROP(Jellyfin::DTO::ImageTypeClass::Value, enableImageTypes, EnableImageTypes);
FWDPROP(bool, enableImages, EnableImages) FWDPROP(bool, enableImages, EnableImages)
FWDPROP(bool, enableTotalRecordCount, EnableTotalRecordCount) FWDPROP(bool, enableTotalRecordCount, EnableTotalRecordCount)
FWDPROP(bool, enableUserData, EnableUserData) FWDPROP(bool, enableUserData, EnableUserData)
FWDPROP(QStringList, excludeItemTypes, ExcludeItemTypes) FWDPROP(QStringList, excludeItemTypes, ExcludeItemTypes)
FWDPROP(QList<Jellyfin::DTO::ItemFieldsClass::Value>, fields, Fields) FWDLISTPROP(Jellyfin::DTO::ItemFieldsClass::Value, fields, Fields)
FWDPROP(qint32, imageTypeLimit, ImageTypeLimit) FWDPROP(qint32, imageTypeLimit, ImageTypeLimit)
FWDPROP(QStringList, includeItemTypes, IncludeItemTypes) FWDPROP(QStringList, includeItemTypes, IncludeItemTypes)
FWDPROP(QStringList, mediaTypes, MediaTypes) FWDPROP(QStringList, mediaTypes, MediaTypes)
@ -219,10 +244,10 @@ public:
FWDPROP(QString, seriesId, SeriesId) FWDPROP(QString, seriesId, SeriesId)
FWDPROP(QString, adjacentTo, AdjacentTo) FWDPROP(QString, adjacentTo, AdjacentTo)
FWDPROP(QList<Jellyfin::DTO::ImageTypeClass::Value>, enableImageTypes, EnableImageTypes) FWDLISTPROP(Jellyfin::DTO::ImageTypeClass::Value, enableImageTypes, EnableImageTypes)
FWDPROP(bool, enableImages, EnableImages) FWDPROP(bool, enableImages, EnableImages)
FWDPROP(bool, enableUserData, EnableUserData) FWDPROP(bool, enableUserData, EnableUserData)
FWDPROP(QList<Jellyfin::DTO::ItemFieldsClass::Value>, fields, Fields) FWDLISTPROP(Jellyfin::DTO::ItemFieldsClass::Value, fields, Fields)
FWDPROP(qint32, imageTypeLimit, ImageTypeLimit) FWDPROP(qint32, imageTypeLimit, ImageTypeLimit)
FWDPROP(bool, isMissing, IsMissing) FWDPROP(bool, isMissing, IsMissing)
FWDPROP(bool, isSpecialSeason, IsSpecialSeason) FWDPROP(bool, isSpecialSeason, IsSpecialSeason)
@ -239,7 +264,7 @@ public:
FWDPROP(QString, adjacentTo, AdjacentTo) FWDPROP(QString, adjacentTo, AdjacentTo)
FWDPROP(bool, enableImages, EnableImages) FWDPROP(bool, enableImages, EnableImages)
FWDPROP(bool, enableUserData, EnableUserData) FWDPROP(bool, enableUserData, EnableUserData)
FWDPROP(QList<Jellyfin::DTO::ItemFieldsClass::Value>, fields, Fields) FWDLISTPROP(Jellyfin::DTO::ItemFieldsClass::Value, fields, Fields)
FWDPROP(qint32, imageTypeLimit, ImageTypeLimit) FWDPROP(qint32, imageTypeLimit, ImageTypeLimit)
FWDPROP(bool, isMissing, IsMissing) FWDPROP(bool, isMissing, IsMissing)
FWDPROP(qint32, season, Season) FWDPROP(qint32, season, Season)
@ -248,6 +273,23 @@ public:
FWDPROP(QString, startItemId, StartItemId) FWDPROP(QString, startItemId, StartItemId)
}; };
using NextUpLoaderBase = AbstractUserParameterLoader<Model::Item, DTO::BaseItemDto, DTO::BaseItemDtoQueryResult, Jellyfin::Loader::GetNextUpParams>;
class NextUpLoader : public NextUpLoaderBase {
Q_OBJECT
public:
explicit NextUpLoader(QObject *parent = nullptr);
FWDPROP(bool, disableFirstEpisode, DisableFirstEpisode)
FWDLISTPROP(Jellyfin::DTO::ImageTypeClass::Value, enableImageTypes, EnableImageTypes);
FWDPROP(bool, enableImges, EnableImges)
FWDPROP(bool, enableTotalRecordCount, EnableTotalRecordCount)
FWDPROP(bool, enableUserData, EnableUserData)
FWDLISTPROP(Jellyfin::DTO::ItemFieldsClass::Value, fields, Fields)
FWDPROP(qint32, imageTypeLimit, ImageTypeLimit)
FWDPROP(QString, parentId, ParentId)
FWDPROP(QString, seriesId, SeriesId)
};
/** /**
* @brief Base class for each model that works with items. * @brief Base class for each model that works with items.
@ -278,6 +320,7 @@ public:
runTimeTicks, runTimeTicks,
artists, artists,
isFolder, isFolder,
overview,
parentIndexNumber, parentIndexNumber,
userDataRating, userDataRating,
userDataPlayedPercentage, userDataPlayedPercentage,
@ -317,6 +360,7 @@ public:
JFRN(runTimeTicks), JFRN(runTimeTicks),
JFRN(artists), JFRN(artists),
JFRN(isFolder), JFRN(isFolder),
JFRN(overview),
JFRN(parentIndexNumber), JFRN(parentIndexNumber),
JFRN(userDataRating), JFRN(userDataRating),
JFRN(userDataPlayedPercentage), JFRN(userDataPlayedPercentage),

View file

@ -60,7 +60,7 @@ public:
: QObject(parent), m_apiClient(apiClient) {} : QObject(parent), m_apiClient(apiClient) {}
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient WRITE setApiClient NOTIFY apiClientChanged STORED false) 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(Jellyfin::ViewModel::LoaderBase::Status status READ status NOTIFY statusChanged STORED false)
Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged STORED false) Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged STORED false)
Q_PROPERTY(bool autoReload MEMBER m_autoReload NOTIFY autoReloadChanged) Q_PROPERTY(bool autoReload MEMBER m_autoReload NOTIFY autoReloadChanged)
Q_PROPERTY(QObject *data READ data NOTIFY dataChanged STORED false) Q_PROPERTY(QObject *data READ data NOTIFY dataChanged STORED false)
@ -73,7 +73,7 @@ public:
void setApiClient(ApiClient *newApiClient); void setApiClient(ApiClient *newApiClient);
void setExtraFields(const QStringList &extraFields); void setExtraFields(const QStringList &extraFields);
signals: signals:
void statusChanged(Status newStatus); void statusChanged(Jellyfin::ViewModel::LoaderBase::Status newStatus);
void apiClientChanged(ApiClient *newApiClient); void apiClientChanged(ApiClient *newApiClient);
void errorStringChanged(QString newErrorString); void errorStringChanged(QString newErrorString);
void autoReloadChanged(bool newAutoReload); void autoReloadChanged(bool newAutoReload);

View file

@ -438,7 +438,7 @@ void ApiClient::generateDeviceProfile() {
clientCapabilities->setIconUrl("https://chris.netsoj.nl/static/img/logo.png"); clientCapabilities->setIconUrl("https://chris.netsoj.nl/static/img/logo.png");
clientCapabilities->setSupportsPersistentIdentifier(true); clientCapabilities->setSupportsPersistentIdentifier(true);
clientCapabilities->setSupportsSync(false); clientCapabilities->setSupportsSync(false);
clientCapabilities->setSupportsMediaControl(false); clientCapabilities->setSupportsMediaControl(true);
clientCapabilities->setSupportsContentUploading(false); clientCapabilities->setSupportsContentUploading(false);
d->clientCapabilities = clientCapabilities; d->clientCapabilities = clientCapabilities;

View file

@ -23,6 +23,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include "JellyfinQt/dto/useritemdatadto.h" #include "JellyfinQt/dto/useritemdatadto.h"
#include "JellyfinQt/dto/userdto.h" #include "JellyfinQt/dto/userdto.h"
Q_LOGGING_CATEGORY(jellyfinApiModel, "jellyfin.apimodel")
namespace Jellyfin { namespace Jellyfin {
// BaseApiModel // BaseApiModel
@ -46,7 +48,7 @@ void BaseModelLoader::componentComplete() {
void BaseModelLoader::autoReloadIfNeeded() { void BaseModelLoader::autoReloadIfNeeded() {
if (m_autoReload && canReload()) { if (m_autoReload && canReload()) {
qDebug() << "reloading due to 'autoReloadIfNeeded()'"; qCDebug(jellyfinApiModel) << "reloading due to 'autoReloadIfNeeded()'";
emit reloadWanted(); emit reloadWanted();
} }
} }
@ -57,14 +59,14 @@ void BaseModelLoader::setApiClient(ApiClient *newApiClient) {
if (changed) { if (changed) {
emit apiClientChanged(newApiClient); emit apiClientChanged(newApiClient);
} }
autoReloadIfNeeded();
} }
void BaseModelLoader::setLimit(int newLimit) { void BaseModelLoader::setLimit(int newLimit) {
int oldLimit = this->m_limit; m_explicitLimitSet = newLimit >= 0;
m_limit = newLimit; qCDebug(jellyfinApiModel) << "Limit explicitly set to " << newLimit;
if (oldLimit != newLimit) { this->m_limit = newLimit;
emit limitChanged(this->m_limit); emit limitChanged(newLimit);
}
} }
void BaseModelLoader::setAutoReload(bool newAutoReload) { void BaseModelLoader::setAutoReload(bool newAutoReload) {
@ -90,7 +92,7 @@ bool BaseModelLoader::canReload() const {
} }
void BaseApiModel::reload() { void BaseApiModel::reload() {
qWarning() << " BaseApiModel slot called instead of overloaded method"; qCWarning(jellyfinApiModel) << " BaseApiModel slot called instead of overloaded method";
} }
// Parameters injectors and result extractors // Parameters injectors and result extractors

View file

@ -75,6 +75,7 @@ void JellyfinPlugin::registerTypes(const char *uri) {
qmlRegisterType<ViewModel::ResumeItemsLoader>(uri, 1, 0, "ResumeItemsLoader"); qmlRegisterType<ViewModel::ResumeItemsLoader>(uri, 1, 0, "ResumeItemsLoader");
qmlRegisterType<ViewModel::ShowSeasonsLoader>(uri, 1, 0, "ShowSeasonsLoader"); qmlRegisterType<ViewModel::ShowSeasonsLoader>(uri, 1, 0, "ShowSeasonsLoader");
qmlRegisterType<ViewModel::ShowEpisodesLoader>(uri, 1, 0, "ShowEpisodesLoader"); qmlRegisterType<ViewModel::ShowEpisodesLoader>(uri, 1, 0, "ShowEpisodesLoader");
qmlRegisterType<ViewModel::NextUpLoader>(uri, 1, 0, "NextUpLoader");
qmlRegisterType<ViewModel::PublicUsersLoader>(uri, 1, 0, "PublicUsersLoader"); qmlRegisterType<ViewModel::PublicUsersLoader>(uri, 1, 0, "PublicUsersLoader");
// Enumerations // Enumerations

View file

@ -144,6 +144,14 @@ void Playlist::appendToList(ViewModel::ItemModel &model) {
reshuffle(); reshuffle();
} }
void Playlist::appendToList(QSharedPointer<Model::Item> item) {
int start = m_list.size();
emit beforeItemsAddedToList(start, 1);
m_list.append(item);
emit itemsAddedToList();
reshuffle();
}
void Playlist::reshuffle() { void Playlist::reshuffle() {
if (m_shuffler->canShuffleInAdvance()) { if (m_shuffler->canShuffleInAdvance()) {
m_shuffler->shuffleInAdvance(); m_shuffler->shuffleInAdvance();

View file

@ -54,6 +54,9 @@ ShowSeasonsLoader::ShowSeasonsLoader(QObject *parent)
ShowEpisodesLoader::ShowEpisodesLoader(QObject *parent) ShowEpisodesLoader::ShowEpisodesLoader(QObject *parent)
: ShowEpisodesLoaderBase(new Jellyfin::Loader::HTTP::GetEpisodesLoader(), parent) {} : ShowEpisodesLoaderBase(new Jellyfin::Loader::HTTP::GetEpisodesLoader(), parent) {}
NextUpLoader::NextUpLoader(QObject *parent)
: NextUpLoaderBase(new Jellyfin::Loader::HTTP::GetNextUpLoader(), parent) {}
ItemModel::ItemModel(QObject *parent) ItemModel::ItemModel(QObject *parent)
: ApiModel<Model::Item>(parent) { } : ApiModel<Model::Item>(parent) { }
@ -86,6 +89,7 @@ QVariant ItemModel::data(const QModelIndex &index, int role) const {
JF_CASE(artists) JF_CASE(artists)
case RoleNames::isFolder: case RoleNames::isFolder:
return QVariant(item->isFolder().value_or(false)); return QVariant(item->isFolder().value_or(false));
JF_CASE(overview)
case RoleNames::parentIndexNumber: case RoleNames::parentIndexNumber:
return QVariant(item->parentIndexNumber().value_or(1)); return QVariant(item->parentIndexNumber().value_or(1));
// UserData // UserData

View file

@ -156,7 +156,9 @@ void PlaybackManager::mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus ne
m_mediaPlayer->setPosition(m_resumePosition / MS_TICK_FACTOR); m_mediaPlayer->setPosition(m_resumePosition / MS_TICK_FACTOR);
} }
} else if (newStatus == QMediaPlayer::EndOfMedia) { } else if (newStatus == QMediaPlayer::EndOfMedia) {
next(); if (m_queue->hasNext() && m_queue->totalSize() > 1) {
next();
}
} }
} }
@ -178,6 +180,8 @@ void PlaybackManager::updatePlaybackInfo() {
} }
void PlaybackManager::playItem(Item *item) { void PlaybackManager::playItem(Item *item) {
m_queue->clearList();
m_queue->appendToList(item->data());
setItem(item->data()); setItem(item->data());
emit hasNextChanged(m_queue->hasNext()); emit hasNextChanged(m_queue->hasNext());
emit hasPreviousChanged(m_queue->hasPrevious()); emit hasPreviousChanged(m_queue->hasPrevious());

View file

@ -38,9 +38,9 @@ set(sailfin_QML_SOURCES
qml/components/UserGridDelegate.qml qml/components/UserGridDelegate.qml
qml/components/VideoPlayer.qml qml/components/VideoPlayer.qml
qml/components/VideoTrackSelector.qml qml/components/VideoTrackSelector.qml
qml/cover/CoverPage.qml qml/cover/CollectionPage.qml
qml/cover/PosterCover.qml qml/cover/PosterCover.qml
qml/cover/VideoCover.qml qml/cover/NowPlayingCover.qml
qml/pages/LegalPage.qml qml/pages/LegalPage.qml
qml/pages/MainPage.qml qml/pages/MainPage.qml
qml/pages/AboutPage.qml qml/pages/AboutPage.qml

View file

@ -87,7 +87,7 @@ PanelBackground {
Rectangle { Rectangle {
id: playQueueShim id: playQueueShim
anchors.fill: albumArt anchors.fill: albumArt
color: Theme.rgba(Theme.overlayBackgroundColor, Theme.opacityLow) color: Theme.rgba(Theme.overlayBackgroundColor, Theme.opacityOverlay)
opacity: 0 opacity: 0
} }
Loader { Loader {
@ -159,6 +159,7 @@ PanelBackground {
} }
icon.source: "image://theme/icon-m-shuffle" icon.source: "image://theme/icon-m-shuffle"
opacity: 0 opacity: 0
enabled: false
onClicked: Notices.show(qsTr("Shuffle not yet implemented")) onClicked: Notices.show(qsTr("Shuffle not yet implemented"))
} }
@ -330,7 +331,7 @@ PanelBackground {
target: nextButton; opacity: 1; enabled: true; target: nextButton; opacity: 1; enabled: true;
} }
PropertyChanges { PropertyChanges {
target: playModeButton; opacity: 1; //enabled: true; target: playModeButton; opacity: 1; enabled: true;
} }
PropertyChanges { PropertyChanges {
target: queueButton; opacity: 1; enabled: true; target: queueButton; opacity: 1; enabled: true;

View file

@ -31,7 +31,7 @@ CoverBackground {
readonly property real rowHeight: height / 2 readonly property real rowHeight: height / 2
readonly property real bottomOffset: width - rowHeight readonly property real bottomOffset: width - rowHeight
readonly property bool onMainScreen: appWindow.itemData === null readonly property bool onMainScreen: appWindow.itemData === null
readonly property bool itemId: appWindow.itemData.jellyfinId || appWindow.pageStack.currentPage.itemId readonly property string itemId: appWindow.pageStack.currentPage.itemId
readonly property bool hasParent: !appWindow.itemData !== null && appWindow.itemData.jellyfinId.length !== 0 readonly property bool hasParent: !appWindow.itemData !== null && appWindow.itemData.jellyfinId.length !== 0
J.ItemModel { J.ItemModel {
@ -39,14 +39,14 @@ CoverBackground {
loader: J.UserItemsLoader { loader: J.UserItemsLoader {
id: randomItemsLoader id: randomItemsLoader
apiClient: appWindow.apiClient apiClient: appWindow.apiClient
limit: cover.rowCount * 2 limit: cover.rowCount * 2 - 2
imageTypes: [J.ImageType.Primary] imageTypes: [J.ImageType.Primary]
sortBy: "IsFavoriteOrLiked,Random" sortBy: "IsFavoriteOrLiked,Random"
recursive: false recursive: false
parentId: hasParent ? itemId : "" parentId: itemId
autoReload: false autoReload: false
onParentIdChanged: { onParentIdChanged: {
if (parentId.length > 0) reload() reload()
} }
} }
} }
@ -82,12 +82,15 @@ CoverBackground {
x: -rowHeight * rowOffset x: -rowHeight * rowOffset
y: rowHeight * 0.5 y: rowHeight * 0.5
} }
PathPercent {
value: 1
}
} }
delegate: RemoteImage { delegate: RemoteImage {
height: rowHeight height: rowHeight
width: height width: height
source: model.jellyfinId source: model.jellyfinId
? Utils.itemModelImageUrl(appWindow.apiClient.baseUrl, model.jellyfinId, model.imageTags["Primary"], "Primary", {"maxHeight": row1.height}) ? Utils.itemModelImageUrl(appWindow.apiClient.baseUrl, model.jellyfinId, model.imageTags["Primary"], "Primary", {"maxHeight": rowHeight})
: "" : ""
blurhash: model.jellyfinId blurhash: model.jellyfinId
? model.imageBlurHashes["Primary"][model.imageTags["Primary"]] ? model.imageBlurHashes["Primary"][model.imageTags["Primary"]]
@ -96,14 +99,18 @@ CoverBackground {
} }
} }
Rectangle { /*Rectangle {
anchors.fill: parent anchors.fill: parent
color: Theme.rgba(Theme.overlayBackgroundColor, Theme.opacityHigh) color: Theme.rgba(Theme.overlayBackgroundColor, Theme.opacityHigh)
} }*/
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width width: parent.width
opacity: randomItemsLoader.status === J.ModelStatus.Ready ? 0.0 : 1.0
Behavior on opacity {
NumberAnimation {}
}
Image { Image {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
source: Qt.resolvedUrl("../icon.png") source: Qt.resolvedUrl("../icon.png")
@ -122,13 +129,12 @@ CoverBackground {
target: appWindow.pageStack target: appWindow.pageStack
onCurrentPageChanged: { onCurrentPageChanged: {
console.log("Reloading cover collection") console.log("Reloading cover collection")
/*randomItems1Loader.parentId = Qt.binding(function() { return onMainScreen ? "" : appWindow.itemData.jellyfinId; })
randomItems2Loader.parentId = Qt.binding(function() { return onMainScreen ? "" : appWindow.itemData.jellyfinId; })*/
//randomItems1.reload()
//randomItems2.reload()
} }
} }
Component.onCompleted: randomItems.reload()
Timer { Timer {
running: true running: true
interval: 5000 interval: 5000

View file

@ -28,19 +28,6 @@ import "../components"
PosterCover { PosterCover {
readonly property var player: appWindow.playbackManager readonly property var player: appWindow.playbackManager
// Wanted to display the currently running move on here, but it's hard :/
/*Rectangle {
anchors.fill: parent
color: "black"
VideoOutput {
id: coverOutput
anchors.fill: parent
source: player
}
}*/
Shim { Shim {
anchors { anchors {
left: parent.left left: parent.left
@ -51,8 +38,27 @@ PosterCover {
} }
CoverActionList { CoverActionList {
enabled: player.hasNext
CoverAction {
iconSource: player.playbackState === MediaPlayer.PlayingState ? "image://theme/icon-cover-pause"
: "image://theme/icon-cover-play"
onTriggered: {
if (player.playbackState === MediaPlayer.PlayingState) {
player.pause()
} else {
player.play()
}
}
}
CoverAction {
iconSource: "image://theme/icon-cover-next-song"
onTriggered: player.next();
}
}
CoverActionList {
enabled: !player.hasNext
CoverAction { CoverAction {
id: playPause
iconSource: player.playbackState === MediaPlayer.PlayingState ? "image://theme/icon-cover-pause" iconSource: player.playbackState === MediaPlayer.PlayingState ? "image://theme/icon-cover-pause"
: "image://theme/icon-cover-play" : "image://theme/icon-cover-play"
onTriggered: { onTriggered: {

View file

@ -27,7 +27,7 @@ import ".."
CoverBackground { CoverBackground {
// Due QTBUG-10822, declarartions such as `property J.Item foo` are not possible. // Due QTBUG-10822, declarartions such as `property J.Item foo` are not possible.
property var mData: appWindow.itemData property var mData: appWindow.playbackManager.item
RemoteImage { RemoteImage {
anchors.fill: parent anchors.fill: parent
source: mData === null ? "" : Utils.itemImageUrl(appWindow.apiClient.baseUrl, mData, "Primary", {"maxWidth": parent.width}) source: mData === null ? "" : Utils.itemImageUrl(appWindow.apiClient.baseUrl, mData, "Primary", {"maxWidth": parent.width})
@ -78,7 +78,7 @@ CoverBackground {
Label { Label {
visible: typeof mData.runTimeTicks !== "undefined" visible: typeof mData.runTimeTicks !== "undefined"
color: Theme.secondaryColor color: Theme.secondaryColor
text: Utils.ticksToText(mData.runTimeTicks) text: qsTr("%1/%2").arg(Utils.timeToText(appWindow.playbackManager.position)).arg(Utils.ticksToText(mData.runTimeTicks))
} }
} }
} }

View file

@ -81,11 +81,9 @@ ApplicationWindow {
cover: { cover: {
// Disabled due to buggy Loader behaviour // Disabled due to buggy Loader behaviour
if ([MediaPlayer.NoMedia, MediaPlayer.InvalidMedia, MediaPlayer.UnknownStatus].indexOf(_playbackManager.mediaStatus) >= 0) { if ([MediaPlayer.NoMedia, MediaPlayer.InvalidMedia, MediaPlayer.UnknownStatus].indexOf(_playbackManager.mediaStatus) >= 0) {
return Qt.resolvedUrl("cover/CoverPage.qml") return Qt.resolvedUrl("cover/CollectionPage.qml")
} else if (playbackManager.hasVideo){
return Qt.resolvedUrl("cover/VideoCover.qml")
} else { } else {
return Qt.resolvedUrl("cover/CoverPage.qml") return Qt.resolvedUrl("cover/NowPlayingCover.qml")
} }
} }

View file

@ -30,6 +30,8 @@ import "../"
Page { Page {
/// True if the models on this page already have been loaded and don't necessarily need a refresh /// True if the models on this page already have been loaded and don't necessarily need a refresh
property bool _modelsLoaded: false property bool _modelsLoaded: false
// Only for cover page
readonly property string itemId: ""
id: mainPage id: mainPage
allowedOrientations: Orientation.All allowedOrientations: Orientation.All
@ -48,7 +50,7 @@ Page {
text: qsTr("Reload") text: qsTr("Reload")
onClicked: loadModels(true) onClicked: loadModels(true)
} }
busy: mediaLibraryLoader.status === J.UsersViewsLoader.Loading busy: mediaLibraryLoader.status === J.ModelStatus.Loading
} }
} }
@ -80,7 +82,7 @@ Page {
//- Section header for films and TV shows that an user hasn't completed yet. //- Section header for films and TV shows that an user hasn't completed yet.
text: qsTr("Resume watching") text: qsTr("Resume watching")
clickable: false clickable: false
busy: userResumeLoader.status === J.UsersViewsLoader.Loading busy: userResumeLoader.status === J.ModelStatus.Loading
Loader { Loader {
width: parent.width width: parent.width
sourceComponent: carrouselView sourceComponent: carrouselView
@ -102,7 +104,7 @@ Page {
//- Section header for next episodes in a TV show that an user was watching. //- Section header for next episodes in a TV show that an user was watching.
text: qsTr("Next up") text: qsTr("Next up")
clickable: false clickable: false
//busy: showNextUpModel.status === .Loading busy: showNextUpLoader.status === J.ModelStatus.Loading
Loader { Loader {
width: parent.width width: parent.width
@ -112,8 +114,11 @@ Page {
J.ItemModel { J.ItemModel {
id: showNextUpModel id: showNextUpModel
/*apiClient: appWindow.apiClient loader: J.NextUpLoader {
limit: 12*/ id: showNextUpLoader
apiClient: appWindow.apiClient
enableUserData: true
}
} }
} }
} }

View file

@ -103,7 +103,7 @@ Page {
if (status === PageStatus.Active) { if (status === PageStatus.Active) {
console.log("Page ready, ItemID: ", itemId, ", UserID: ", apiClient.userId) console.log("Page ready, ItemID: ", itemId, ", UserID: ", apiClient.userId)
jItemLoader.autoReload = true jItemLoader.autoReload = true
//appWindow.itemData = jItemLoader.data appWindow.itemData = Qt.binding(function() { return jItemLoader.data; })
} }
} }
} }