From 7b6c272aa9bc694be1cb3e0943811f4d5131ab22 Mon Sep 17 00:00:00 2001 From: Henk Kalkwater Date: Wed, 11 Aug 2021 23:35:33 +0200 Subject: [PATCH] Rewire more of Sailfish frontend into new backend This should encompass most simple things, besides some larger, trickier things, like the video streams and the now-broken userdata --- core/CMakeLists.txt | 2 + core/include/JellyfinQt/apiclient.h | 4 +- core/include/JellyfinQt/apimodel.h | 37 +++--- core/include/JellyfinQt/credentialmanager.h | 1 - core/include/JellyfinQt/model/item.h | 22 ++-- core/include/JellyfinQt/model/playlist.h | 2 +- core/include/JellyfinQt/viewmodel/item.h | 58 ++++++++-- core/include/JellyfinQt/viewmodel/itemmodel.h | 106 ++++++++++++++++++ .../JellyfinQt/viewmodel/playbackmanager.h | 5 +- core/include/JellyfinQt/viewmodel/userdata.h | 81 +++++++++++++ core/src/apiclient.cpp | 17 +-- core/src/apimodel.cpp | 4 + core/src/credentialmanager.cpp | 5 - core/src/jellyfin.cpp | 4 + core/src/model/item.cpp | 44 +++----- core/src/model/playlist.cpp | 4 +- core/src/viewmodel/item.cpp | 26 ++++- core/src/viewmodel/itemmodel.cpp | 27 ++++- core/src/viewmodel/playbackmanager.cpp | 5 + core/src/viewmodel/userdata.cpp | 39 +++++++ sailfish/CMakeLists.txt | 9 +- sailfish/harbour-sailfin.desktop | 2 +- sailfish/qml/ApiClient.qml | 7 -- sailfish/qml/Constants.qml | 2 +- sailfish/qml/components/PlaybackBar.qml | 25 +++-- sailfish/qml/components/VideoPlayer.qml | 49 ++++---- .../qml/components/videoplayer/VideoError.qml | 5 +- .../qml/components/videoplayer/VideoHud.qml | 5 +- sailfish/qml/harbour-sailfin.qml | 22 ++-- sailfish/qml/pages/AboutPage.qml | 2 +- sailfish/qml/pages/MainPage.qml | 36 +++--- sailfish/qml/pages/SettingsPage.qml | 13 ++- sailfish/qml/pages/VideoPage.qml | 4 +- .../qml/pages/itemdetails/BaseDetailPage.qml | 21 ++-- .../qml/pages/itemdetails/CollectionPage.qml | 19 ++-- .../qml/pages/itemdetails/MusicAlbumPage.qml | 19 ++-- sailfish/qml/pages/itemdetails/PhotoPage.qml | 4 +- sailfish/qml/pages/itemdetails/SeasonPage.qml | 23 ++-- sailfish/qml/pages/itemdetails/SeriesPage.qml | 19 ++-- .../qml/pages/itemdetails/UnsupportedPage.qml | 2 +- sailfish/qml/pages/itemdetails/VideoPage.qml | 2 +- sailfish/qml/pages/settings/DebugPage.qml | 6 +- .../pages/setup/AddServerConnectingPage.qml | 41 ++++--- sailfish/qml/pages/setup/AddServerPage.qml | 68 +++++------ sailfish/qml/pages/setup/LoginDialog.qml | 10 +- sailfish/qml/qmldir | 1 - sailfish/src/harbour-sailfin.cpp | 2 +- 47 files changed, 620 insertions(+), 291 deletions(-) create mode 100644 core/include/JellyfinQt/viewmodel/userdata.h create mode 100644 core/src/viewmodel/userdata.cpp delete mode 100644 sailfish/qml/ApiClient.qml diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 26cc0d6..8d8da3c 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -19,6 +19,7 @@ set(JellyfinQt_SOURCES src/viewmodel/modelstatus.cpp src/viewmodel/playbackmanager.cpp src/viewmodel/playlist.cpp + src/viewmodel/userdata.cpp src/apiclient.cpp src/apimodel.cpp src/credentialmanager.cpp @@ -46,6 +47,7 @@ set(JellyfinQt_HEADERS include/JellyfinQt/viewmodel/propertyhelper.h include/JellyfinQt/viewmodel/playbackmanager.h include/JellyfinQt/viewmodel/playlist.h + include/JellyfinQt/viewmodel/userdata.h include/JellyfinQt/apiclient.h include/JellyfinQt/apimodel.h include/JellyfinQt/credentialmanager.h diff --git a/core/include/JellyfinQt/apiclient.h b/core/include/JellyfinQt/apiclient.h index 001b63a..5173432 100644 --- a/core/include/JellyfinQt/apiclient.h +++ b/core/include/JellyfinQt/apiclient.h @@ -124,7 +124,7 @@ public: Q_ENUM(ApiError) const QString &baseUrl() const { return this->m_baseUrl; } - const QString &userId() const { return m_userId; } + const QString userId() const { return m_userId; } const QString &deviceId() const { return m_deviceId; } /** * @brief QML applications can set this type to indicate which commands they support. @@ -275,7 +275,7 @@ private: QString m_token; QString m_deviceName; QString m_deviceId; - QString m_userId = ""; + QString m_userId; QJsonObject m_deviceProfile; QJsonObject m_playbackDeviceProfile; bool m_online = true; diff --git a/core/include/JellyfinQt/apimodel.h b/core/include/JellyfinQt/apimodel.h index 86e8fa1..657ddf8 100644 --- a/core/include/JellyfinQt/apimodel.h +++ b/core/include/JellyfinQt/apimodel.h @@ -188,7 +188,7 @@ public: * @return pair containing the items loaded and the integer containing the starting offset. A starting * offset of -1 means an error has occurred. */ - std::pair, int> &&result() { return std::move(m_result); } + std::pair, int> &&result() { return std::move(m_result); } protected: /** * @brief Loads data from the given offset with a maximum count of limit. @@ -205,7 +205,7 @@ protected: m_startIndex = startIndex; m_totalRecordCount = totalRecordCount; } - std::pair, int> m_result; + std::pair, int> m_result; }; /** @@ -275,7 +275,7 @@ protected: // 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 }; + this->m_result = { QList(), -1 }; if (!setRequestStartIndex

(this->m_parameters, offset) && suggestedModelStatus == ViewModel::ModelStatus::LoadingMore) { @@ -326,12 +326,12 @@ protected: if (totalRecordCount < 0) { totalRecordCount = records.size(); } - QList models; + QList models; models.reserve(records.size()); // Convert the DTOs into models for (int i = 0; i < records.size(); i++) { - models.append(T(records[i], m_loader->apiClient())); + models.append(new T(records[i], m_loader->apiClient())); } this->setStatus(ViewModel::ModelStatus::Ready); this->m_result = { models, this->m_startIndex}; @@ -441,7 +441,7 @@ public: } // QList-like API - const T& at(int index) const { return m_array.at(index); } + QSharedPointer at(int index) const { return m_array.at(index); } /** * @return the amount of objects in this model. */ @@ -449,22 +449,23 @@ public: return m_array.size(); } - void insert(int index, T &object) { + void insert(int index, QSharedPointer object) { Q_ASSERT(index >= 0 && index <= size()); this->beginInsertRows(QModelIndex(), index, index); m_array.insert(index, object); this->endInsertRows(); } - void append(T &object) { insert(size(), object); } - void append(QList &objects) { + void append(QSharedPointer object) { insert(size(), object); } + //void append(T &object) { insert(size(), object); } + void append(QList> &objects) { int index = size(); this->beginInsertRows(QModelIndex(), index, index + objects.size() - 1); m_array.append(objects); this->endInsertRows(); }; - QList mid(int pos, int length = -1) { + QList mid(int pos, int length = -1) { return m_array.mid(pos, length); } @@ -483,7 +484,7 @@ public: this->endRemoveRows(); } - void removeOne(T &object) { + void removeOne(QSharedPointer object) { int idx = m_array.indexOf(object); if (idx >= 0) { removeAt(idx); @@ -535,20 +536,26 @@ public: protected: // Model-specific properties. - QList m_array; + QList> m_array; ModelLoader *m_loader = nullptr; void loadingFinished() override { Q_ASSERT(m_loader != nullptr); - std::pair, int> result = m_loader->result(); + 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()) { - append(result.first); + m_array.reserve(m_array.size() + result.second); + for (auto it = result.first.begin(); it != result.first.end(); it++) { + append(QSharedPointer(*it)); + } } else if (result.second < m_array.size()){ removeUntilEnd(result.second); - append(result.first); + m_array.reserve(m_array.size() + result.second); + for (auto it = result.first.begin(); it != result.first.end(); it++) { + append(QSharedPointer(*it)); + } } else { // result.second > m_array.size() qWarning() << "Retrieved data from loader at position bigger than size()"; diff --git a/core/include/JellyfinQt/credentialmanager.h b/core/include/JellyfinQt/credentialmanager.h index 872287a..7881eec 100644 --- a/core/include/JellyfinQt/credentialmanager.h +++ b/core/include/JellyfinQt/credentialmanager.h @@ -123,7 +123,6 @@ public: private: QString urlToGroupName(const QString &url) const; - QString groupNameToUrl(const QString &group) const; QSettings m_settings; }; diff --git a/core/include/JellyfinQt/model/item.h b/core/include/JellyfinQt/model/item.h index 9675a58..5f3ba50 100644 --- a/core/include/JellyfinQt/model/item.h +++ b/core/include/JellyfinQt/model/item.h @@ -20,9 +20,11 @@ #ifndef JELLYFIN_MODEL_ITEM #define JELLYFIN_MODEL_ITEM +#include +#include + +#include #include -#include -#include #include "../dto/baseitemdto.h" #include "../support/loader.h" @@ -31,20 +33,22 @@ namespace Jellyfin { namespace Model { -class Item : public DTO::BaseItemDto { +class Item : public QObject, public DTO::BaseItemDto { + Q_OBJECT public: + using UserDataChangedCallback = std::function; /** * @brief Constructor that creates an empty item. */ - Item(); + Item(QObject *parent = nullptr); /** * @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); - virtual ~Item(); + Item(const DTO::BaseItemDto &data, ApiClient *apiClient = nullptr, QObject *parent = nullptr); + //virtual ~Item(); /** * @brief sameAs Returns true if this item represents the same item as `other` @@ -58,11 +62,13 @@ public: bool sameAs(const DTO::BaseItemDto &other); void setApiClient(ApiClient *apiClient); + +signals: + void userDataChanged(const DTO::UserItemDataDto &newUserData); private: ApiClient *m_apiClient = nullptr; - QList m_apiClientConnections; + void updateUserData(const QString &itemId, const DTO::UserItemDataDto &userData); - void onUserDataUpdated(const QString &itemId, const DTO::UserItemDataDto &userData); }; } diff --git a/core/include/JellyfinQt/model/playlist.h b/core/include/JellyfinQt/model/playlist.h index 3e58e32..c829ac0 100644 --- a/core/include/JellyfinQt/model/playlist.h +++ b/core/include/JellyfinQt/model/playlist.h @@ -76,7 +76,7 @@ public: /** * @brief Appends all items from the given itemModel to this list */ - void appendToList(const ViewModel::ItemModel &model); + void appendToList(ViewModel::ItemModel &model); /** * @brief Start playing this playlist diff --git a/core/include/JellyfinQt/viewmodel/item.h b/core/include/JellyfinQt/viewmodel/item.h index bac8590..9fabf39 100644 --- a/core/include/JellyfinQt/viewmodel/item.h +++ b/core/include/JellyfinQt/viewmodel/item.h @@ -46,15 +46,22 @@ #include "loader.h" namespace Jellyfin { + +namespace DTO { +class UserItemDataDto; +} // NS DTO + namespace ViewModel { +class UserData; + class Item : public QObject { Q_OBJECT public: explicit Item(QObject *parent = nullptr, QSharedPointer data = QSharedPointer::create()); // 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(QString jellyfinId READ jellyfinId NOTIFY jellyfinIdChanged) Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QString originalTitle READ originalTitle NOTIFY originalTitleChanged) Q_PROPERTY(QString serverId READ serverId NOTIFY serverIdChanged) @@ -81,27 +88,29 @@ public: //SKIP: ExternalUrls //SKIP: MediaSources Q_PROPERTY(float criticRating READ criticRating NOTIFY criticRatingChanged) - Q_PROPERTY(QStringList productionLocations READ productionLocations NOTIFY productionLocationsChanged) + Q_PROPERTY(QStringList productionLocations READ productionLocations NOTIFY productionLocationsChanged)*/ // Handpicked, important ones - Q_PROPERTY(qint64 runTimeTicks READ runTimeTicks NOTIFY runTimeTicksChanged)*/ + Q_PROPERTY(qint64 runTimeTicks READ runTimeTicks NOTIFY runTimeTicksChanged) Q_PROPERTY(QString overview READ overview NOTIFY overviewChanged) Q_PROPERTY(int productionYear READ productionYear NOTIFY productionYearChanged) Q_PROPERTY(int indexNumber READ indexNumber NOTIFY indexNumberChanged) Q_PROPERTY(int indexNumberEnd READ indexNumberEnd NOTIFY indexNumberEndChanged) Q_PROPERTY(bool isFolder READ isFolder NOTIFY isFolderChanged) Q_PROPERTY(QString type READ type NOTIFY typeChanged) - /*Q_PROPERTY(QString parentBackdropItemId READ parentBackdropItemId NOTIFY parentBackdropItemIdChanged) + Q_PROPERTY(QString parentBackdropItemId READ parentBackdropItemId NOTIFY parentBackdropItemIdChanged) Q_PROPERTY(QStringList parentBackdropImageTags READ parentBackdropImageTags NOTIFY parentBackdropImageTagsChanged) Q_PROPERTY(UserData *userData READ userData NOTIFY userDataChanged) Q_PROPERTY(int recursiveItemCount READ recursiveItemCount NOTIFY recursiveItemCountChanged) Q_PROPERTY(int childCount READ childCount NOTIFY childCountChanged) Q_PROPERTY(QString albumArtist READ albumArtist NOTIFY albumArtistChanged) - Q_PROPERTY(QVariantList albumArtists READ albumArtists NOTIFY albumArtistsChanged) + /*Q_PROPERTY(QVariantList albumArtists READ albumArtists NOTIFY albumArtistsChanged)*/ Q_PROPERTY(QString seriesName READ seriesName NOTIFY seriesNameChanged) + Q_PROPERTY(QString seriesId READ seriesId NOTIFY seriesIdChanged) + Q_PROPERTY(QString seasonId READ seasonId NOTIFY seasonIdChanged) Q_PROPERTY(QString seasonName READ seasonName NOTIFY seasonNameChanged) - Q_PROPERTY(QList __list__mediaStreams MEMBER __list__m_mediaStreams NOTIFY mediaStreamsChanged) - Q_PROPERTY(QVariantList mediaStreams READ mediaStreams NOTIFY mediaStreamsChanged STORED false) + /*Q_PROPERTY(QList __list__mediaStreams MEMBER __list__m_mediaStreams NOTIFY mediaStreamsChanged) + Q_PROPERTY(QVariantList mediaStreams READ mediaStreams NOTIFY mediaStreamsChanged STORED false)*/ Q_PROPERTY(QStringList artists READ artists NOTIFY artistsChanged) // Why is this a QJsonObject? Well, because I couldn't be bothered to implement the deserialisations of // a QHash at the moment. @@ -110,9 +119,9 @@ public: Q_PROPERTY(QJsonObject imageBlurHashes READ imageBlurHashes NOTIFY imageBlurHashesChanged) Q_PROPERTY(QString mediaType READ mediaType READ mediaType NOTIFY mediaTypeChanged) Q_PROPERTY(int width READ width NOTIFY widthChanged) - Q_PROPERTY(int height READ height NOTIFY heightChanged)*/ + Q_PROPERTY(int height READ height NOTIFY heightChanged) - QUuid jellyfinId() const { return m_data->jellyfinId(); } + QString jellyfinId() const { return m_data->jellyfinId(); } QString name() const { return m_data->name(); } QString originalTitle() const { return m_data->originalTitle(); } QString serverId() const { return m_data->serverId(); } @@ -125,17 +134,36 @@ public: int airsBeforeSeasonNumber() const { return m_data->airsBeforeSeasonNumber().value_or(0); } int airsAfterSeasonNumber() const { return m_data->airsAfterSeasonNumber().value_or(999); } int airsBeforeEpisodeNumber() const { return m_data->airsBeforeEpisodeNumber().value_or(0); } + qint64 runTimeTicks() const { return m_data->runTimeTicks().value_or(0); } QString overview() const { return m_data->overview(); } int productionYear() const { return m_data->productionYear().value_or(0); } int indexNumber() const { return m_data->indexNumber().value_or(-1); } int indexNumberEnd() const { return m_data->indexNumberEnd().value_or(-1); } bool isFolder() const { return m_data->isFolder().value_or(false); } QString type() const { return m_data->type(); } + QString parentBackdropItemId() const { return m_data->parentBackdropItemId(); } + QStringList parentBackdropImageTags() const { return m_data->parentBackdropImageTags(); } + UserData *userData() const { return m_userData; } + int recursiveItemCount() const { return m_data->recursiveItemCount().value_or(0); } + int childCount() const { return m_data->childCount().value_or(0); } + QString albumArtist() const { return m_data->albumArtist(); } + QString seriesName() const { return m_data->seriesName(); } + QString seriesId() const { return m_data->seriesId(); } + QString seasonId() const { return m_data->seasonId(); } + QString seasonName() const { return m_data->seasonName(); } + + QStringList artists() const { return m_data->artists(); } + QJsonObject imageTags() const { return m_data->imageTags(); } + QStringList backdropImageTags() const { return m_data->backdropImageTags(); } + QJsonObject imageBlurHashes() const { return m_data->imageBlurHashes(); } + QString mediaType() const { return m_data->mediaType(); } + int width() const { return m_data->width().value_or(0); } + int height() const { return m_data->height().value_or(0); } QSharedPointer data() const { return m_data; } void setData(QSharedPointer newData); signals: - void jellyfinIdChanged(const QUuid &newId); + void jellyfinIdChanged(const QString &newId); void nameChanged(const QString &newName); void originalTitleChanged(const QString &newOriginalTitle); void serverIdChanged(const QString &newServerId); @@ -171,12 +199,14 @@ signals: void typeChanged(const QString &newType); void parentBackdropItemIdChanged(); void parentBackdropImageTagsChanged(); - //void userDataChanged(UserData *newUserData); + void userDataChanged(UserData *newUserData); void recursiveItemCountChanged(int newRecursiveItemCount); void childCountChanged(int newChildCount); void albumArtistChanged(const QString &newAlbumArtist); //void albumArtistsChanged(NameGuidPair *newAlbumArtists); void seriesNameChanged(const QString &newSeriesName); + void seriesIdChanged(const QString &newSeriesId); + void seasonIdChanged(const QString &newSeasonId); void seasonNameChanged(const QString &newSeasonName); void mediaStreamsChanged(/*const QList &newMediaStreams*/); void artistsChanged(const QStringList &newArtists); @@ -187,7 +217,13 @@ signals: void widthChanged(int newWidth); void heightChanged(int newHeight); protected: + void setUserData(DTO::UserItemDataDto &newData); + void setUserData(QSharedPointer newData); + QSharedPointer m_data; + UserData *m_userData = nullptr; +private slots: + void onUserDataChanged(const DTO::UserItemDataDto &userData); }; class ItemLoader : public Loader { diff --git a/core/include/JellyfinQt/viewmodel/itemmodel.h b/core/include/JellyfinQt/viewmodel/itemmodel.h index 330931c..2d8ba68 100644 --- a/core/include/JellyfinQt/viewmodel/itemmodel.h +++ b/core/include/JellyfinQt/viewmodel/itemmodel.h @@ -134,12 +134,108 @@ public: FWDPROP(QList, excludeLocationTypes, ExcludeLocationTypes) FWDPROP(QList, fields, Fields) FWDPROP(QList, filters, Filters) + FWDPROP(QStringList, genreIds, GenreIds) + FWDPROP(QStringList, genres, Genres) + FWDPROP(bool, hasImdbId, HasImdbId) + FWDPROP(bool, hasOfficialRating, HasOfficialRating) + FWDPROP(bool, hasOverview, HasOverview) + FWDPROP(bool, hasParentalRating, HasParentalRating) + FWDPROP(bool, hasSpecialFeature, HasSpecialFeature) + FWDPROP(bool, hasSubtitles, HasSubtitles) + FWDPROP(bool, hasThemeSong, HasThemeSong) + FWDPROP(bool, hasThemeVideo, HasThemeVideo) + FWDPROP(bool, hasTmdbId, HasTmdbId) + FWDPROP(bool, hasTrailer, HasTrailer) + FWDPROP(bool, hasTvdbId, HasTvdbId) + FWDPROP(QStringList, ids, Ids) + FWDPROP(qint32, imageTypeLimit, ImageTypeLimit) + FWDPROP(QList, imageTypes, ImageTypes) + FWDPROP(QStringList, includeItemTypes, IncludeItemTypes) + FWDPROP(bool, is3D, Is3D) + FWDPROP(bool, is4K, Is4K) + FWDPROP(bool, isFavorite, IsFavorite) + FWDPROP(bool, isHd, IsHd) + FWDPROP(bool, isLocked, IsLocked) + FWDPROP(bool, isMissing, IsMissing) + FWDPROP(bool, isPlaceHolder, IsPlaceHolder) + FWDPROP(bool, isPlayed, IsPlayed) + FWDPROP(bool, isUnaired, IsUnaired) + FWDPROP(QList, locationTypes, LocationTypes) + FWDPROP(qint32, maxHeight, MaxHeight) + FWDPROP(QString, maxOfficialRating, MaxOfficialRating) + FWDPROP(QDateTime, maxPremiereDate, MaxPremiereDate) + FWDPROP(qint32, maxWidth, MaxWidth) + FWDPROP(QStringList, mediaTypes, MediaTypes) + FWDPROP(qint32, minHeight, MinHeight) + FWDPROP(QString, minOfficialRating, MinOfficialRating) + FWDPROP(QDateTime, minPremiereDate, MinPremiereDate) + FWDPROP(qint32, minWidth, MinWidth) + FWDPROP(QString, sortBy, SortBy) + FWDPROP(QString, sortOrder, SortOrder) + FWDPROP(QStringList, tags, Tags) + FWDPROP(QList, years, Years) FWDPROP(QString, parentId, ParentId) FWDPROP(bool, recursive, Recursive) + FWDPROP(QString, searchTerm, SearchTerm) //FWDPROP(bool, collapseBoxSetItems) }; +using ResumeItemsLoaderBase = AbstractUserParameterLoader; +class ResumeItemsLoader : public ResumeItemsLoaderBase { + Q_OBJECT +public: + explicit ResumeItemsLoader(QObject *parent = nullptr); + + FWDPROP(QList, enableImageTypes, EnableImageTypes); + FWDPROP(bool, enableImages, EnableImages) + FWDPROP(bool, enableTotalRecordCount, EnableTotalRecordCount) + FWDPROP(bool, enableUserData, EnableUserData) + FWDPROP(QStringList, excludeItemTypes, ExcludeItemTypes) + FWDPROP(QList, fields, Fields) + FWDPROP(qint32, imageTypeLimit, ImageTypeLimit) + FWDPROP(QStringList, includeItemTypes, IncludeItemTypes) + FWDPROP(QStringList, mediaTypes, MediaTypes) + FWDPROP(QString, parentId, ParentId) + FWDPROP(QString, searchTerm, SearchTerm) +}; + +using ShowSeasonsLoaderBase = AbstractUserParameterLoader; +class ShowSeasonsLoader : public ShowSeasonsLoaderBase { + Q_OBJECT +public: + explicit ShowSeasonsLoader(QObject *parent = nullptr); + + FWDPROP(QString, seriesId, SeriesId) + FWDPROP(QString, adjacentTo, AdjacentTo) + FWDPROP(QList, enableImageTypes, EnableImageTypes) + FWDPROP(bool, enableImages, EnableImages) + FWDPROP(bool, enableUserData, EnableUserData) + FWDPROP(QList, fields, Fields) + FWDPROP(qint32, imageTypeLimit, ImageTypeLimit) + FWDPROP(bool, isMissing, IsMissing) + FWDPROP(bool, isSpecialSeason, IsSpecialSeason) + +}; + +using ShowEpisodesLoaderBase = AbstractUserParameterLoader; +class ShowEpisodesLoader : public ShowEpisodesLoaderBase { + Q_OBJECT +public: + explicit ShowEpisodesLoader(QObject *parent = nullptr); + + FWDPROP(QString, seriesId, SeriesId) + FWDPROP(QString, adjacentTo, AdjacentTo) + FWDPROP(bool, enableImages, EnableImages) + FWDPROP(bool, enableUserData, EnableUserData) + FWDPROP(QList, fields, Fields) + FWDPROP(qint32, imageTypeLimit, ImageTypeLimit) + FWDPROP(bool, isMissing, IsMissing) + FWDPROP(qint32, season, Season) + FWDPROP(QString, seasonId, SeasonId) + FWDPROP(QString, sortBy, SortBy) + FWDPROP(QString, startItemId, StartItemId) +}; /** @@ -167,6 +263,11 @@ public: mediaType, type, collectionType, + indexNumber, + runTimeTicks, + artists, + isFolder, + parentIndexNumber, jellyfinExtendModelAfterHere = Qt::UserRole + 300 // Should be enough for now }; @@ -191,6 +292,11 @@ public: JFRN(mediaType), JFRN(type), JFRN(collectionType), + JFRN(indexNumber), + JFRN(runTimeTicks), + JFRN(artists), + JFRN(isFolder), + JFRN(parentIndexNumber), }; } QVariant data(const QModelIndex &index, int role) const override; diff --git a/core/include/JellyfinQt/viewmodel/playbackmanager.h b/core/include/JellyfinQt/viewmodel/playbackmanager.h index d4703f7..0081788 100644 --- a/core/include/JellyfinQt/viewmodel/playbackmanager.h +++ b/core/include/JellyfinQt/viewmodel/playbackmanager.h @@ -91,6 +91,7 @@ public: Q_PROPERTY(QMediaPlayer::Error error READ error NOTIFY errorChanged) Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged) Q_PROPERTY(bool hasVideo READ hasVideo NOTIFY hasVideoChanged) + Q_PROPERTY(bool seekable READ seekable NOTIFY seekableChanged) Q_PROPERTY(QObject* mediaObject READ mediaObject NOTIFY mediaObjectChanged) Q_PROPERTY(QMediaPlayer::MediaStatus mediaStatus READ mediaStatus NOTIFY mediaStatusChanged) Q_PROPERTY(QMediaPlayer::State playbackState READ playbackState NOTIFY playbackStateChanged) @@ -108,9 +109,10 @@ public: int queueIndex() const { return m_queueIndex; } // Current media player related property getters - QMediaPlayer::State playbackState() const { return m_playbackState; } + QMediaPlayer::State playbackState() const { return m_mediaPlayer->state()/*m_playbackState*/; } QMediaPlayer::MediaStatus mediaStatus() const { return m_mediaPlayer->mediaStatus(); } bool hasVideo() const { return m_mediaPlayer->isVideoAvailable(); } + bool seekable() const { return m_mediaPlayer->isSeekable(); } QMediaPlayer::Error error () const { return m_mediaPlayer->error(); } QString errorString() const { return m_mediaPlayer->errorString(); } signals: @@ -132,6 +134,7 @@ signals: void playbackStateChanged(QMediaPlayer::State newState); void mediaStatusChanged(QMediaPlayer::MediaStatus newMediaStatus); void hasVideoChanged(bool newHasVideo); + void seekableChanged(bool newSeekable); void errorChanged(QMediaPlayer::Error newError); void errorStringChanged(const QString &newErrorString); public slots: diff --git a/core/include/JellyfinQt/viewmodel/userdata.h b/core/include/JellyfinQt/viewmodel/userdata.h new file mode 100644 index 0000000..1df6ea6 --- /dev/null +++ b/core/include/JellyfinQt/viewmodel/userdata.h @@ -0,0 +1,81 @@ +/* + * 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_VIEWMODEL_USERDATA_H +#define JELLYFIN_VIEWMODEL_USERDATA_H + +#include +#include +#include + +#include "../dto/useritemdatadto.h" + +namespace Jellyfin { +namespace ViewModel{ + +class UserData : public QObject { + Q_OBJECT +public: + explicit UserData(QObject* parent = nullptr); + explicit UserData(QSharedPointer data, QObject* parent = nullptr); + + void setData(QSharedPointer data); + + Q_PROPERTY(double rating READ rating NOTIFY ratingChanged) + Q_PROPERTY(double playedPercentage READ playedPercentage NOTIFY playedPercentageChanged) + Q_PROPERTY(int unplayedItemCount READ unplayedItemCount NOTIFY unplayedItemCountChanged) + Q_PROPERTY(qint64 playbackPositionTicks READ playbackPositionTicks NOTIFY playbackPositionTicksChanged); + Q_PROPERTY(int playCount READ playCount NOTIFY playCountChanged) + Q_PROPERTY(bool favorite READ favorite NOTIFY favoriteChanged) + Q_PROPERTY(bool m_likes READ likes NOTIFY likesChanged) + Q_PROPERTY(QDateTime lastPlayedDate READ lastPlayedDate NOTIFY lastPlayedDateChanged) + Q_PROPERTY(bool played READ played NOTIFY playedChanged) + Q_PROPERTY(QString key READ key NOTIFY keyChanged) + + double rating() const { return m_data->rating().value_or(0); } + double playedPercentage() const { return m_data->playedPercentage().value_or(0); } + int unplayedItemCount() const { return m_data->unplayedItemCount().value_or(0); } + qint64 playbackPositionTicks() const { return m_data->playbackPositionTicks(); } + int playCount() const { return m_data->playCount(); } + bool favorite() const { return m_data->isFavorite(); } + bool likes() const { return m_data->likes().value_or(false); } + QDateTime lastPlayedDate() const { return m_data->lastPlayedDate(); } + bool played() const { return m_data->played(); } + QString key() const { return m_data->key(); } + +signals: + void ratingChanged(double newRating); + void playedPercentageChanged(double newPlayedPercentage); + void unplayedItemCountChanged(int newUnplayedItemCount); + void playbackPositionTicksChanged(qint64 newPlaybackPositionTicks); + void playCountChanged(int newPlayCount); + void favoriteChanged(bool newFavorite); + void likesChanged(bool newLikes); + void lastPlayedDateChanged(QDateTime newLastPlayedDate); + void playedChanged(bool newPLayed); + void keyChanged(QString newKey); + +private: + QSharedPointer m_data; +}; + + +} // NS ViewModel +} // NS Jellyfin + +#endif // JELLYFIN_VIEWMODEL_USERDATA_H diff --git a/core/src/apiclient.cpp b/core/src/apiclient.cpp index 7b89285..c2a087e 100644 --- a/core/src/apiclient.cpp +++ b/core/src/apiclient.cpp @@ -35,6 +35,7 @@ ApiClient::ApiClient(QObject *parent) m_credManager = CredentialsManager::newInstance(this); connect(m_credManager, &CredentialsManager::serversListed, this, &ApiClient::credManagerServersListed); connect(m_credManager, &CredentialsManager::usersListed, this, &ApiClient::credManagerUsersListed); + connect(m_credManager, &CredentialsManager::tokenRetrieved, this, &ApiClient::credManagerTokenRetrieved); generateDeviceProfile(); } @@ -118,20 +119,14 @@ void ApiClient::credManagerUsersListed(const QString &server, QStringList users) QString user = users[0]; qDebug() << "Chosen user: " << user; - QObject *ctx3 = new QObject(this); - connect(m_credManager, &CredentialsManager::tokenRetrieved, ctx3, [this, ctx3] - (const QString &server, const QString &user, const QString &token) { - Q_UNUSED(server) - this->m_token = token; - this->setUserId(user); - this->setAuthenticated(true); - this->postCapabilities(); - disconnect(ctx3); - }, Qt::UniqueConnection); m_credManager->get(server, user); } void ApiClient::credManagerTokenRetrieved(const QString &server, const QString &user, const QString &token) { - + this->m_token = token; + qDebug() << "Token retreived, logged in as user " << user; + this->setUserId(user); + this->setAuthenticated(true); + this->postCapabilities(); } void ApiClient::setupConnection() { diff --git a/core/src/apimodel.cpp b/core/src/apimodel.cpp index d47407f..c818aee 100644 --- a/core/src/apimodel.cpp +++ b/core/src/apimodel.cpp @@ -71,6 +71,10 @@ void BaseModelLoader::setAutoReload(bool newAutoReload) { if (m_autoReload != newAutoReload) { m_autoReload = newAutoReload; emit autoReloadChanged(newAutoReload); + + if (canReload()) { + reload(); + } } } diff --git a/core/src/credentialmanager.cpp b/core/src/credentialmanager.cpp index e307aa6..dad214c 100644 --- a/core/src/credentialmanager.cpp +++ b/core/src/credentialmanager.cpp @@ -42,11 +42,6 @@ QString FallbackCredentialsManager::urlToGroupName(const QString &url) const { return QString::number(qHash(url), 16); } -QString FallbackCredentialsManager::groupNameToUrl(const QString &group) const { - QString tmp = QString(group); - return tmp.replace('|', "/"); -} - void FallbackCredentialsManager::store(const QString &server, const QString &user, const QString &token) { m_settings.setValue(urlToGroupName(server) + "/users/" + user + "/accessToken", token); m_settings.setValue(urlToGroupName(server) + "/address", server); diff --git a/core/src/jellyfin.cpp b/core/src/jellyfin.cpp index e47c5e9..ab222cc 100644 --- a/core/src/jellyfin.cpp +++ b/core/src/jellyfin.cpp @@ -38,11 +38,15 @@ void registerTypes(const char *uri) { qmlRegisterType(uri, 1, 0, "LatestMediaLoader"); qmlRegisterType(uri, 1, 0, "UserItemsLoader"); qmlRegisterType(uri, 1, 0, "UsersViewsLoader"); + qmlRegisterType(uri, 1, 0, "ResumeItemsLoader"); + qmlRegisterType(uri, 1, 0, "ShowSeasonsLoader"); + qmlRegisterType(uri, 1, 0, "ShowEpisodesLoader"); // Enumerations qmlRegisterUncreatableType(uri, 1, 0, "GeneralCommandType", "Is an enum"); qmlRegisterUncreatableType(uri, 1, 0, "ModelStatus", "Is an enum"); qmlRegisterUncreatableType(uri, 1, 0, "PlayMethod", "Is an enum"); + qmlRegisterUncreatableType(uri, 1, 0, "ItemFields", "Is an enum"); qRegisterMetaType(); } diff --git a/core/src/model/item.cpp b/core/src/model/item.cpp index f2de2e5..1a4362b 100644 --- a/core/src/model/item.cpp +++ b/core/src/model/item.cpp @@ -23,25 +23,14 @@ namespace Jellyfin { namespace Model { -Item::Item() - : Item(DTO::BaseItemDto(), nullptr) { } +Item::Item(QObject *parent) + : Item(DTO::BaseItemDto(), nullptr, parent) { } -Item::Item(const DTO::BaseItemDto &data, ApiClient *apiClient) - : DTO::BaseItemDto(data), m_apiClient(apiClient) { - if (m_apiClient != nullptr) { - m_apiClientConnections.append(QObject::connect( - m_apiClient->eventbus(), - &EventBus::itemUserDataUpdated, - [&](auto itemId, auto userData) { - onUserDataUpdated(itemId, userData); - })); - } -} - -Item::~Item() { - for(auto it = m_apiClientConnections.begin(); it != m_apiClientConnections.end(); it++) { - QObject::disconnect(*it); - } +Item::Item(const DTO::BaseItemDto &data, ApiClient *apiClient, QObject *parent) + : DTO::BaseItemDto(data), + QObject(parent), + m_apiClient(nullptr) { + setApiClient(apiClient); } bool Item::sameAs(const DTO::BaseItemDto &other) { @@ -50,23 +39,20 @@ bool Item::sameAs(const DTO::BaseItemDto &other) { void Item::setApiClient(ApiClient *apiClient) { if (m_apiClient != nullptr) { - for(auto it = m_apiClientConnections.begin(); it != m_apiClientConnections.end(); it++) { - QObject::disconnect(*it); - } - m_apiClientConnections.clear(); + disconnect(m_apiClient->eventbus(), &EventBus::itemUserDataUpdated, + this, &Item::updateUserData); } this->m_apiClient = apiClient; if (apiClient != nullptr) { - m_apiClientConnections.append(QObject::connect( - m_apiClient->eventbus(), - &EventBus::itemUserDataUpdated, - [&](auto itemId, auto userData) { - onUserDataUpdated(itemId, userData); - })); + QObject::connect(m_apiClient->eventbus(), &EventBus::itemUserDataUpdated, + this, &Item::updateUserData); } } -void Item::onUserDataUpdated(const QString &itemId, const DTO::UserItemDataDto &userData) { +void Item::updateUserData(const QString &itemId, const DTO::UserItemDataDto &userData) { + if (itemId == this->jellyfinId()) { + emit userDataChanged(userData); + } } } diff --git a/core/src/model/playlist.cpp b/core/src/model/playlist.cpp index 06f5ad4..e8d8c37 100644 --- a/core/src/model/playlist.cpp +++ b/core/src/model/playlist.cpp @@ -103,12 +103,12 @@ QSharedPointer Playlist::nextItem() { return m_nextItem; } -void Playlist::appendToList(const ViewModel::ItemModel &model) { +void Playlist::appendToList(ViewModel::ItemModel &model) { int start = m_list.size(); int count = model.size(); m_list.reserve(count); for (int i = 0; i < count; i++) { - m_list.append(QSharedPointer::create(model.at(i))); + m_list.append(QSharedPointer(model.at(i))); } emit itemsAddedToList(start, count); reshuffle(); diff --git a/core/src/viewmodel/item.cpp b/core/src/viewmodel/item.cpp index 0dc0dc4..c82cc46 100644 --- a/core/src/viewmodel/item.cpp +++ b/core/src/viewmodel/item.cpp @@ -17,17 +17,39 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "JellyfinQt/viewmodel/item.h" +#include "JellyfinQt/viewmodel/userdata.h" namespace Jellyfin { namespace ViewModel { Item::Item(QObject *parent, QSharedPointer data) - : QObject(parent), m_data(data){ - + : QObject(parent), + m_data(data), + m_userData(new UserData(this)){ + connect(m_data.data(), &Model::Item::userDataChanged, this, &Item::onUserDataChanged); } void Item::setData(QSharedPointer newData) { + if (!m_data.isNull()) { + disconnect(m_data.data(), &Model::Item::userDataChanged, this, &Item::onUserDataChanged); + } m_data = newData; + if (!m_data.isNull()) { + connect(m_data.data(), &Model::Item::userDataChanged, this, &Item::onUserDataChanged); + } +} + +void Item::setUserData(DTO::UserItemDataDto &newData) { + setUserData(QSharedPointer::create(newData)); +} + +void Item::setUserData(QSharedPointer newData) { + m_userData->setData(newData); + emit userDataChanged(m_userData); +} + +void Item::onUserDataChanged(const DTO::UserItemDataDto &newData) { + setUserData(QSharedPointer::create(newData)); } diff --git a/core/src/viewmodel/itemmodel.cpp b/core/src/viewmodel/itemmodel.cpp index d45c372..fba60ba 100644 --- a/core/src/viewmodel/itemmodel.cpp +++ b/core/src/viewmodel/itemmodel.cpp @@ -18,12 +18,15 @@ */ #include "JellyfinQt/viewmodel/itemmodel.h" +#include "JellyfinQt/loader/http/getepisodes.h" #include "JellyfinQt/loader/http/getlatestmedia.h" #include "JellyfinQt/loader/http/getitemsbyuserid.h" +#include "JellyfinQt/loader/http/getresumeitems.h" +#include "JellyfinQt/loader/http/getseasons.h" #define JF_CASE(roleName) case roleName: \ try { \ - return QVariant(item.roleName()); \ + return QVariant(item->roleName()); \ } catch(std::bad_optional_access &e) { \ return QVariant(); \ } @@ -41,6 +44,15 @@ LatestMediaLoader::LatestMediaLoader(QObject *parent) UserItemsLoader::UserItemsLoader(QObject *parent) : UserItemsLoaderBase(new Jellyfin::Loader::HTTP::GetItemsByUserIdLoader(), parent) {} +ResumeItemsLoader::ResumeItemsLoader(QObject *parent) + : ResumeItemsLoaderBase(new Jellyfin::Loader::HTTP::GetResumeItemsLoader(), parent) {} + +ShowSeasonsLoader::ShowSeasonsLoader(QObject *parent) + : ShowSeasonsLoaderBase(new Jellyfin::Loader::HTTP::GetSeasonsLoader(), parent) {} + +ShowEpisodesLoader::ShowEpisodesLoader(QObject *parent) + : ShowEpisodesLoaderBase(new Jellyfin::Loader::HTTP::GetEpisodesLoader(), parent) {} + ItemModel::ItemModel(QObject *parent) : ApiModel(parent) { } @@ -48,7 +60,7 @@ QVariant ItemModel::data(const QModelIndex &index, int role) const { if (role <= Qt::UserRole || !index.isValid()) return QVariant(); int row = index.row(); if (row < 0 || row >= m_array.size()) return QVariant(); - Model::Item item = m_array[row]; + QSharedPointer item = m_array[row]; switch(role) { JF_CASE(jellyfinId) JF_CASE(name) @@ -66,6 +78,15 @@ QVariant ItemModel::data(const QModelIndex &index, int role) const { JF_CASE(mediaType) JF_CASE(type) JF_CASE(collectionType) + case RoleNames::indexNumber: + return QVariant(item->indexNumber().value_or(0)); + case RoleNames::runTimeTicks: + return QVariant(item->runTimeTicks().value_or(0)); + JF_CASE(artists) + case RoleNames::isFolder: + return QVariant(item->isFolder().value_or(false)); + case RoleNames::parentIndexNumber: + return QVariant(item->parentIndexNumber().value_or(1)); default: return QVariant(); } @@ -73,7 +94,7 @@ QVariant ItemModel::data(const QModelIndex &index, int role) const { } QSharedPointer ItemModel::itemAt(int index) { - return QSharedPointer::create(m_array[index]); + return m_array[index]; } } // NS ViewModel diff --git a/core/src/viewmodel/playbackmanager.cpp b/core/src/viewmodel/playbackmanager.cpp index 2d06b7e..3a93fe0 100644 --- a/core/src/viewmodel/playbackmanager.cpp +++ b/core/src/viewmodel/playbackmanager.cpp @@ -56,6 +56,7 @@ PlaybackManager::PlaybackManager(QObject *parent) connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::mediaPlayerDurationChanged); connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged); connect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged); + connect(m_mediaPlayer, &QMediaPlayer::seekableChanged, this, &PlaybackManager::seekableChanged); // I do not like the complicated overload cast connect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::Error)), this, SLOT(mediaPlayerError(QMediaPlayer::Error))); @@ -142,6 +143,7 @@ void PlaybackManager::mediaPlayerStateChanged(QMediaPlayer::State newState) { postPlaybackInfo(Progress); } m_oldState = newState; + emit playbackStateChanged(newState); } void PlaybackManager::mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus) { @@ -352,6 +354,9 @@ void ItemUrlFetcherThread::run() { playMethod = PlayMethod::DirectPlay; } else if (source.supportsDirectStream()) { QString mediaType = item->mediaType(); + if (mediaType == "Video") { + mediaType.append('s'); + } QUrlQuery query; query.addQueryItem("mediaSourceId", source.jellyfinId()); query.addQueryItem("deviceId", m_parent->m_apiClient->deviceId()); diff --git a/core/src/viewmodel/userdata.cpp b/core/src/viewmodel/userdata.cpp new file mode 100644 index 0000000..6c5f5a7 --- /dev/null +++ b/core/src/viewmodel/userdata.cpp @@ -0,0 +1,39 @@ +/* + * 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 + */ +#include + +namespace Jellyfin { +namespace ViewModel { + +UserData::UserData(QObject *parent) + : QObject(parent), + m_data(QSharedPointer::create()) { +} + +UserData::UserData(QSharedPointer data, QObject *parent) + : QObject(parent), + m_data(data) { +} + +void UserData::setData(QSharedPointer data) { + m_data = data; +} + +} // NS ViewModel +} // NS Jellyfin diff --git a/sailfish/CMakeLists.txt b/sailfish/CMakeLists.txt index 77df1cf..839da7e 100644 --- a/sailfish/CMakeLists.txt +++ b/sailfish/CMakeLists.txt @@ -18,7 +18,6 @@ set(harbour-sailfin_SOURCES src/harbour-sailfin.cpp) set(sailfin_QML_SOURCES - qml/ApiClient.qml qml/Constants.qml qml/Utils.js qml/components/music/NarrowAlbumCover.qml @@ -27,8 +26,8 @@ set(sailfin_QML_SOURCES qml/components/videoplayer/VideoError.qml qml/components/videoplayer/VideoHud.qml qml/components/IconListItem.qml - qml/components/JItem.qml - qml/components/LibraryItemDelegate.qml + qml/components/JItem.qml + qml/components/LibraryItemDelegate.qml qml/components/MoreSection.qml qml/components/PlainLabel.qml qml/components/PlaybackBar.qml @@ -60,8 +59,8 @@ set(sailfin_QML_SOURCES qml/pages/itemdetails/VideoPage.qml qml/pages/settings/DebugPage.qml qml/pages/setup/AddServerConnectingPage.qml - qml/pages/setup/AddServerPage.qml - qml/pages/setup/LoginDialog.qml + qml/pages/setup/AddServerPage.qml + qml/pages/setup/LoginDialog.qml qml/qmldir) add_executable(harbour-sailfin ${harbour-sailfin_SOURCES} ${sailfin_QML_SOURCES}) diff --git a/sailfish/harbour-sailfin.desktop b/sailfish/harbour-sailfin.desktop index 891bad3..8984d91 100644 --- a/sailfish/harbour-sailfin.desktop +++ b/sailfish/harbour-sailfin.desktop @@ -3,7 +3,7 @@ Type=Application Version=1.1 X-Nemo-Application-Type=silica-qt5 Icon=harbour-sailfin -Exec=harbour-sailfin +Exec=harbour-sailfin --no-attempt-sandbox Name=Sailfin diff --git a/sailfish/qml/ApiClient.qml b/sailfish/qml/ApiClient.qml deleted file mode 100644 index e591c6d..0000000 --- a/sailfish/qml/ApiClient.qml +++ /dev/null @@ -1,7 +0,0 @@ -pragma Singleton -import QtQuick 2.6 -import nl.netsoj.chris.Jellyfin 1.0 as J - -J.ApiClient { - supportedCommands: [J.GeneralCommandType.Play, J.GeneralCommandType.DisplayContent, J.GeneralCommandType.DisplayMessage] -} diff --git a/sailfish/qml/Constants.qml b/sailfish/qml/Constants.qml index 62086e1..48116d9 100644 --- a/sailfish/qml/Constants.qml +++ b/sailfish/qml/Constants.qml @@ -46,7 +46,7 @@ QtObject { } } - readonly property real libraryDelegatePosterHeight: libraryDelegateHeight * 1.6667 + readonly property real libraryDelegatePosterHeight: libraryDelegateHeight * 1.5 // 1.6667 readonly property real libraryProgressHeight: Theme.paddingMedium diff --git a/sailfish/qml/components/PlaybackBar.qml b/sailfish/qml/components/PlaybackBar.qml index 5068537..76ba022 100644 --- a/sailfish/qml/components/PlaybackBar.qml +++ b/sailfish/qml/components/PlaybackBar.qml @@ -21,7 +21,7 @@ import QtQuick 2.6 import QtMultimedia 5.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "../" @@ -30,7 +30,7 @@ import "../" * +---+--------------------------------------+ * |\ /| +---+ | * | \ / | Media title | | | - * | X | | ⏸︎| | + * | X | | ⏸︎ | | * | / \ | Artist 1, artist 2 | | | * |/ \| +---+ | * +-----+------------------------------------+ @@ -40,13 +40,15 @@ PanelBackground { height: Theme.itemSizeLarge width: parent.width y: parent.height - height - property PlaybackManager manager + //FIXME: Once QTBUG-10822 is resolved, change to J.PlaybackManager + property var manager property bool open property real visibleSize: height property bool isFullPage: false property bool showQueue: false property bool _pageWasShowingNavigationIndicator + readonly property bool mediaLoading: [MediaPlayer.Loading, MediaPlayer.Buffering].indexOf(manager.mediaStatus) >= 0 transform: Translate {id: playbackBarTranslate; y: 0} @@ -72,10 +74,11 @@ PanelBackground { } source: largeAlbumArt.source fillMode: Image.PreserveAspectCrop + opacity: 1 Image { id: largeAlbumArt - source: Utils.itemImageUrl(ApiClient.baseUrl, manager.item, "Primary") + source: Utils.itemImageUrl(apiClient.baseUrl, manager.item, "Primary") fillMode: Image.PreserveAspectFit anchors.fill: parent opacity: 0 @@ -119,11 +122,11 @@ PanelBackground { Label { id: artists text: { - if (manager.item == null) return qsTr("Play some media!") - console.log(manager.item.type) - switch(manager.item.type) { + //return manager.item.mediaType; + if (manager.item === null) return qsTr("Play some media!") + switch(manager.item.mediaType) { case "Audio": - return manager.item.artists.join(", ") + return manager.item.artists //.join(", ") } return qsTr("Not audio") } @@ -157,6 +160,7 @@ PanelBackground { icon.source: "image://theme/icon-m-previous" enabled: false opacity: 0 + onClicked: manager.previous() } IconButton { @@ -182,6 +186,7 @@ PanelBackground { icon.source: "image://theme/icon-m-next" enabled: false opacity: 0 + onClicked: manager.next() } IconButton { id: queueButton @@ -206,7 +211,7 @@ PanelBackground { minimumValue: 0 value: manager.position maximumValue: manager.duration - indeterminate: [MediaPlayer.Loading, MediaPlayer.Buffering].indexOf(manager.mediaStatus) >= 0 + indeterminate: mediaLoading } Slider { @@ -352,7 +357,7 @@ PanelBackground { }, State { name: "hidden" - when: (manager.playbackState === MediaPlayer.StoppedState || "__hidePlaybackBar" in pageStack.currentPage) && !isFullPage + when: ((manager.playbackState === MediaPlayer.StoppedState && !mediaLoading) || "__hidePlaybackBar" in pageStack.currentPage) && !isFullPage PropertyChanges { target: playbackBarTranslate // + small padding since the ProgressBar otherwise would stick out diff --git a/sailfish/qml/components/VideoPlayer.qml b/sailfish/qml/components/VideoPlayer.qml index 827e6ff..0fd3579 100644 --- a/sailfish/qml/components/VideoPlayer.qml +++ b/sailfish/qml/components/VideoPlayer.qml @@ -20,7 +20,7 @@ import QtQuick 2.6 import QtMultimedia 5.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "videoplayer" import "../" @@ -31,7 +31,8 @@ import "../" SilicaItem { id: playerRoot - property JellyfinItem item + //FIXME: Once QTBUG-10822 is resolved, change to J.Item + property var item property string title: item.name property bool resume property int progress @@ -39,7 +40,8 @@ SilicaItem { readonly property bool hudVisible: !hud.hidden || manager.error !== MediaPlayer.NoError property int audioTrack: 0 property int subtitleTrack: 0 - property PlaybackManager manager; + //FIXME: Once QTBUG-10822 is resolved, change to J.PlaybackManager + property var manager; // Blackground to prevent the ambience from leaking through Rectangle { @@ -59,21 +61,6 @@ SilicaItem { manager: playerRoot.manager title: videoPlayer.title - Label { - anchors.fill: parent - anchors.margins: Theme.horizontalPageMargin - text: item.jellyfinId + "\n" + appWindow.playbackManager.streamUrl + "\n" - + (manager.playMethod === PlaybackManager.DirectPlay ? "Direct Play" : "Transcoding") + "\n" - + manager.position + "\n" - + manager.mediaStatus + "\n" - // + player.bufferProgress + "\n" - // + player.metaData.videoCodec + "@" + player.metaData.videoFrameRate + "(" + player.metaData.videoBitRate + ")" + "\n" - // + player.metaData.audioCodec + "(" + player.metaData.audioBitRate + ")" + "\n" - // + player.errorString + "\n" - font.pixelSize: Theme.fontSizeExtraSmall - wrapMode: "WordWrap" - visible: appWindow.showDebugInfo - } } VideoError { @@ -81,11 +68,35 @@ SilicaItem { player: manager } + Label { + anchors.fill: parent + anchors.margins: Theme.horizontalPageMargin + text: item.jellyfinId + "\n" + appWindow.playbackManager.streamUrl + "\n" + + (manager.playMethod === J.PlaybackManager.DirectPlay ? "Direct Play" : "Transcoding") + "\n" + + manager.position + "\n" + + manager.mediaStatus + "\n" + // + player.bufferProgress + "\n" + // + player.metaData.videoCodec + "@" + player.metaData.videoFrameRate + "(" + player.metaData.videoBitRate + ")" + "\n" + // + player.metaData.audioCodec + "(" + player.metaData.audioBitRate + ")" + "\n" + // + player.errorString + "\n" + font.pixelSize: Theme.fontSizeExtraSmall + wrapMode: "WordWrap" + visible: appWindow.showDebugInfo + + MouseArea { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + enabled: parent.visible + onClicked: Clipboard.text = appWindow.playbackManager.streamUrl + } + } + function start() { manager.audioIndex = audioTrack manager.subtitleIndex = subtitleTrack manager.resumePlayback = resume - manager.playItem(item.jellyfinId) + manager.playItem(item) } function stop() { diff --git a/sailfish/qml/components/videoplayer/VideoError.qml b/sailfish/qml/components/videoplayer/VideoError.qml index 1a1aa84..4d7aa23 100644 --- a/sailfish/qml/components/videoplayer/VideoError.qml +++ b/sailfish/qml/components/videoplayer/VideoError.qml @@ -20,11 +20,12 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import QtMultimedia 5.6 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J Rectangle { id: videoError - property PlaybackManager player + //FIXME: Once QTBUG-10822 is resolved, change to J.PlaybackManager + property var player color: pal.palette.overlayBackgroundColor opacity: player.error === MediaPlayer.NoError ? 0.0 : 1.0 Behavior on opacity { FadeAnimator {} } diff --git a/sailfish/qml/components/videoplayer/VideoHud.qml b/sailfish/qml/components/videoplayer/VideoHud.qml index 4876f5f..17f35d9 100644 --- a/sailfish/qml/components/videoplayer/VideoHud.qml +++ b/sailfish/qml/components/videoplayer/VideoHud.qml @@ -20,7 +20,7 @@ import QtQuick 2.6 import QtMultimedia 5.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "../../Utils.js" as Utils @@ -30,7 +30,8 @@ import "../../Utils.js" as Utils */ Item { id: videoHud - property PlaybackManager manager + //FIXME: Once QTBUG-10822 is resolved, change to J.PlaybackManager + property var manager property string title property bool _manuallyActivated: false readonly property bool hidden: opacity == 0.0 diff --git a/sailfish/qml/harbour-sailfin.qml b/sailfish/qml/harbour-sailfin.qml index 1495079..f464046 100644 --- a/sailfish/qml/harbour-sailfin.qml +++ b/sailfish/qml/harbour-sailfin.qml @@ -35,6 +35,7 @@ ApplicationWindow { // The global mediaPlayer instance //readonly property MediaPlayer mediaPlayer: _mediaPlayer readonly property PlaybackManager playbackManager: _playbackManager + readonly property ApiClient apiClient: _apiClient // Due QTBUG-10822, declarartions such as `property J.Item foo` are not possible. property QtObject itemData @@ -46,20 +47,23 @@ ApplicationWindow { property bool _hidePlaybackBar: false bottomMargin: playbackBar.visibleSize - - + ApiClient { + id: _apiClient + objectName: "Test" + supportedCommands: [J.GeneralCommandType.Play, J.GeneralCommandType.DisplayContent, J.GeneralCommandType.DisplayMessage] + } initialPage: Component { MainPage { Connections { - target: D.ApiClient + target: apiClient // Replace the MainPage if no server was set up. } onStatusChanged: { if (status == PageStatus.Active && !_hasInitialized) { _hasInitialized = true; - D.ApiClient.restoreSavedSession(); + apiClient.restoreSavedSession(); } } } @@ -87,14 +91,9 @@ ApplicationWindow { } } - /*MediaPlayer { - id: _mediaPlayer - autoPlay: true - }*/ - PlaybackManager { id: _playbackManager - apiClient: D.ApiClient + apiClient: appWindow.apiClient audioIndex: 0 autoOpen: true } @@ -111,14 +110,13 @@ ApplicationWindow { PlaybackBar { id: playbackBar manager: _playbackManager - state: "hidden" // CTMBWSIU: Code That Might Break When Silica Is Updated Component.onCompleted: playbackBar.parent = __silica_applicationwindow_instance._rotatingItem } //FIXME: proper error handling Connections { - target: D.ApiClient + target: apiClient onNetworkError: errorNotification.show("Network error: " + error) onConnectionFailed: errorNotification.show("Connect error: " + error) //onConnectionSuccess: errorNotification.show("Success: " + loginMessage) diff --git a/sailfish/qml/pages/AboutPage.qml b/sailfish/qml/pages/AboutPage.qml index 5fe62ec..6ee3b26 100644 --- a/sailfish/qml/pages/AboutPage.qml +++ b/sailfish/qml/pages/AboutPage.qml @@ -54,7 +54,7 @@ Page { "Copyright © Chris Josten 2020

" + "

Sailfin is Free Software licensed under the LGPL-v2.1 or later, at your choice. " + "Parts of the code of Sailfin are from other libraries. View their licenses here.

") - .arg(ApiClient.version) + .arg(apiClient.version) textFormat: Text.StyledText color: Theme.secondaryHighlightColor linkColor: Theme.primaryColor diff --git a/sailfish/qml/pages/MainPage.qml b/sailfish/qml/pages/MainPage.qml index d69dbfb..a2931d5 100644 --- a/sailfish/qml/pages/MainPage.qml +++ b/sailfish/qml/pages/MainPage.qml @@ -23,7 +23,6 @@ import nl.netsoj.chris.Jellyfin 1.0 as J import "../components" import "../" -import "../Utils.js" as Utils /** * Main page, which simply shows some content of every library, as well as next items. @@ -49,7 +48,7 @@ Page { text: qsTr("Reload") onClicked: loadModels(true) } - busy: userViewsLoader.status === J.UsersViewsLoader.Loading + busy: mediaLibraryLoader.status === J.UsersViewsLoader.Loading } } @@ -73,7 +72,7 @@ Page { id: mediaLibraryModel loader: J.UsersViewsLoader { id: mediaLibraryLoader - apiClient: ApiClient + apiClient: appWindow.apiClient } } @@ -81,7 +80,7 @@ Page { //- Section header for films and TV shows that an user hasn't completed yet. text: qsTr("Resume watching") clickable: false - //busy: userResumeModel.status === J.ApiModel.Loading + busy: userResumeLoader.status === J.ApiModel.Loading Loader { width: parent.width sourceComponent: carrouselView @@ -90,10 +89,12 @@ Page { J.ItemModel { id: userResumeModel - // Resume model - /*apiClient: ApiClient - limit: 12 - recursive: true*/ + loader: J.ResumeItemsLoader { + id: userResumeLoader + apiClient: appWindow.apiClient + limit: 12 + //recursive: true*/ + } } } } @@ -111,7 +112,7 @@ Page { J.ItemModel { id: showNextUpModel - /*apiClient: ApiClient + /*apiClient: appWindow.apiClient limit: 12*/ } } @@ -132,7 +133,7 @@ Page { J.ItemModel { id: userItemModel loader: J.LatestMediaLoader { - apiClient: ApiClient + apiClient: appWindow.apiClient parentId: jellyfinId } } @@ -171,7 +172,7 @@ Page { } Connections { - target: ApiClient + target: appWindow.apiClient onAuthenticatedChanged: loadModels(false) } @@ -181,7 +182,7 @@ Page { * even if loaded. */ function loadModels(force) { - if (force || (ApiClient.authenticated && !_modelsLoaded)) { + if (force || (appWindow.apiClient.authenticated && !_modelsLoaded)) { _modelsLoaded = true; mediaLibraryModel.reload() //userResumeModel.reload() @@ -192,14 +193,15 @@ Page { Component { id: carrouselView SilicaListView { + property bool isPortrait: Utils.usePortraitCover(collectionType) id: list clip: true height: { if (count > 0) { - if (["tvshows", "movies"].indexOf(collectionType) == -1) { - Constants.libraryDelegateHeight - } else { + if (isPortrait) { Constants.libraryDelegatePosterHeight + } else { + Constants.libraryDelegateHeight } } else { 0 @@ -217,12 +219,12 @@ Page { delegate: LibraryItemDelegate { property string id: model.jellyfinId title: model.name - poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.jellyfinId, model.imageTags["Primary"], "Primary", {"maxHeight": height}) + poster: Utils.itemModelImageUrl(appWindow.apiClient.baseUrl, model.jellyfinId, model.imageTags["Primary"], "Primary", {"maxHeight": height}) Binding on blurhash { when: poster !== "" value: model.imageBlurHashes["Primary"][model.imageTags["Primary"]] } - landscape: !Utils.usePortraitCover(collectionType) + landscape: !isPortrait progress: (typeof model.userData !== "undefined") ? model.userData.playedPercentage / 100 : 0.0 onClicked: { diff --git a/sailfish/qml/pages/SettingsPage.qml b/sailfish/qml/pages/SettingsPage.qml index 53b2517..c288c5f 100644 --- a/sailfish/qml/pages/SettingsPage.qml +++ b/sailfish/qml/pages/SettingsPage.qml @@ -22,6 +22,7 @@ import Sailfish.Silica 1.0 import nl.netsoj.chris.Jellyfin 1.0 as J import "../components" +import ".." Page { id: settingsPage @@ -69,7 +70,7 @@ Page { top: parent.top bottom: parent.bottom } - source: ApiClient.baseUrl + "/Users/" + ApiClient.userId + "/Images/Primary?tag=" + loggedInUser.primaryImageTag + source: apiClient.baseUrl + "/Users/" + apiClient.userId + "/Images/Primary?tag=" + loggedInUser.primaryImageTag } Label { @@ -80,7 +81,7 @@ Page { bottom: parent.verticalCenter right: parent.right } - text: loggedInUser.status == User.Ready ? loggedInUser.name : ApiClient.userId + text: loggedInUser.status == User.Ready ? loggedInUser.name : apiClient.userId color: Theme.highlightColor } @@ -92,7 +93,7 @@ Page { top: parent.verticalCenter right: parent.right } - text: ApiClient.baseUrl + text: apiClient.baseUrl color: Theme.secondaryHighlightColor } @@ -104,21 +105,23 @@ Page { ButtonLayout { Button { text: qsTr("Log out") - onClicked: remorse.execute(qsTr("Logging out"), ApiClient.deleteSession) + onClicked: remorse.execute(qsTr("Logging out"), apiClient.deleteSession) } } SectionHeader { - //: Other settings + //: Other settings menu item text: qsTr("Other") } IconListItem { + //: Debug information settings menu itemy text: qsTr("Debug information") iconSource: "image://theme/icon-s-developer" onClicked: pageStack.push(Qt.resolvedUrl("settings/DebugPage.qml")) } + //: About Sailfin settings menu itemy IconListItem { text: qsTr("About Sailfin") iconSource: "image://theme/icon-m-about" diff --git a/sailfish/qml/pages/VideoPage.qml b/sailfish/qml/pages/VideoPage.qml index 4ea545b..9a3666c 100644 --- a/sailfish/qml/pages/VideoPage.qml +++ b/sailfish/qml/pages/VideoPage.qml @@ -21,7 +21,7 @@ import Sailfish.Silica 1.0 import "../components" -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J /** * Page only containing a video player. @@ -33,7 +33,7 @@ Page { id: videoPage // PlaybackBar will hide itself when it encounters a page with such a property property bool __hidePlaybackBar: true - property JellyfinItem itemData + property var itemData property int audioTrack property int subtitleTrack property bool resume: true diff --git a/sailfish/qml/pages/itemdetails/BaseDetailPage.qml b/sailfish/qml/pages/itemdetails/BaseDetailPage.qml index e0b067e..36b2f01 100644 --- a/sailfish/qml/pages/itemdetails/BaseDetailPage.qml +++ b/sailfish/qml/pages/itemdetails/BaseDetailPage.qml @@ -22,7 +22,7 @@ import Sailfish.Silica 1.0 import nl.netsoj.chris.Jellyfin 1.0 as J import "../../components" -import "../.." +import "../../" /** * This page displays details about a film, show, season, episode, and so on. @@ -34,8 +34,6 @@ Page { id: pageRoot property string itemId: "" property alias itemData: jItemLoader.data - //property string itemId: "" - //property var itemData: ({}) property bool _loading: jItemLoader.status === J.ItemLoader.Loading readonly property bool hasLogo: (typeof itemData.imageTags !== "undefined") && (typeof itemData.imageTags["Logo"] !== "undefined") property string _chosenBackdropImage: "" @@ -46,10 +44,10 @@ Page { if (itemData.backdropImageTags.length > 0) { rand = Math.floor(Math.random() * (itemData.backdropImageTags.length - 0.001)) console.log("Random: ", rand) - _chosenBackdropImage = ApiClient.baseUrl + "/Items/" + itemId + "/Images/Backdrop/" + rand + "?tag=" +itemData.backdropImageTags[rand] + "&maxHeight" + height + _chosenBackdropImage = apiClient.baseUrl + "/Items/" + itemId + "/Images/Backdrop/" + rand + "?tag=" +itemData.backdropImageTags[rand] + "&maxHeight" + height } else if (itemData.parentBackdropImageTags.length > 0) { rand = Math.floor(Math.random() * (itemData.parentBackdropImageTags.length - 0.001)) - _chosenBackdropImage = ApiClient.baseUrl + "/Items/" + itemData.parentBackdropItemId + "/Images/Backdrop/" + rand + "?tag=" + itemData.parentBackdropImageTags[0] + _chosenBackdropImage = apiClient.baseUrl + "/Items/" + itemData.parentBackdropItemId + "/Images/Backdrop/" + rand + "?tag=" + itemData.parentBackdropImageTags[0] } } @@ -86,8 +84,9 @@ Page { J.ItemLoader { id: jItemLoader - apiClient: ApiClient + apiClient: appWindow.apiClient itemId: pageRoot.itemId + autoReload: false onStatusChanged: { console.log("Status changed: " + newStatus, JSON.stringify(jItemLoader.data)) if (status === J.ItemLoader.Ready) { @@ -96,15 +95,13 @@ Page { } } - Label { - text: "ItemLoader status=%1, \nitemId=%2\nitemData=%3".arg(jItemLoader.status).arg(jItemLoader.itemId).arg(jItemLoader.data) - } - onStatusChanged: { - if (status == PageStatus.Deactivating) { + if (status === PageStatus.Deactivating) { //appWindow.itemData = ({}) } - if (status == PageStatus.Active) { + if (status === PageStatus.Active) { + console.log("Page ready, ItemID: ", itemId, ", UserID: ", apiClient.userId) + jItemLoader.autoReload = true //appWindow.itemData = jItemLoader.data } } diff --git a/sailfish/qml/pages/itemdetails/CollectionPage.qml b/sailfish/qml/pages/itemdetails/CollectionPage.qml index ee90d9d..8e31246 100644 --- a/sailfish/qml/pages/itemdetails/CollectionPage.qml +++ b/sailfish/qml/pages/itemdetails/CollectionPage.qml @@ -29,10 +29,13 @@ BaseDetailPage { J.ItemModel { id: collectionModel - //sortBy: ["SortName"] loader: J.UserItemsLoader { - apiClient: ApiClient + id: collectionLoader + apiClient: appWindow.apiClient parentId: itemData.jellyfinId + autoReload: itemData.jellyfinId.length > 0 + onParentIdChanged: if (parentId.length > 0) reload() + sortBy: "SortName" } } @@ -61,7 +64,7 @@ BaseDetailPage { RemoteImage { id: itemImage anchors.fill: parent - source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.jellyfinId, model.imageTags.Primary, "Primary", {"maxWidth": width}) + source: Utils.itemModelImageUrl(apiClient.baseUrl, model.jellyfinId, model.imageTags.Primary, "Primary", {"maxWidth": width}) blurhash: model.imageBlurHashes.Primary[model.imageTags.Primary] fallbackColor: Utils.colorFromString(model.name) fillMode: Image.PreserveAspectCrop @@ -138,20 +141,20 @@ BaseDetailPage { MenuItem { //: Sort order text: qsTr("Ascending") - onClicked: apply(model.value, ApiModel.Ascending) + onClicked: apply(model.value, "Ascending") } MenuItem { //: Sort order text: qsTr("Descending") - onClicked: apply(model.value, ApiModel.Descending) + onClicked: apply(model.value, "Descending") } } onClicked: openMenu() function apply(field, order) { - collectionModel.sortBy = [field]; - collectionModel.sortOrder = order; - collectionModel.reload() + collectionLoader.sortBy = field; + collectionLoader.sortOrder = order; + collectionLoader.reload() pageStack.pop() } } diff --git a/sailfish/qml/pages/itemdetails/MusicAlbumPage.qml b/sailfish/qml/pages/itemdetails/MusicAlbumPage.qml index bf9be2c..e1c1ba5 100644 --- a/sailfish/qml/pages/itemdetails/MusicAlbumPage.qml +++ b/sailfish/qml/pages/itemdetails/MusicAlbumPage.qml @@ -36,11 +36,12 @@ BaseDetailPage { J.ItemModel { id: collectionModel loader: J.UserItemsLoader { - apiClient: ApiClient - //sortBy: ["SortName"] + apiClient: appWindow.apiClient + sortBy: "SortName" //fields: ["ItemCounts","PrimaryImageAspectRatio","BasicSyncInfo","CanDelete","MediaSourceCount"] parentId: itemData.jellyfinId - onParentIdChanged: reload() + autoReload: itemData.jellyfinId.length > 0 + onParentIdChanged: if (parentId.length > 0) reload() } } RowLayout { @@ -52,9 +53,9 @@ BaseDetailPage { visible: _twoColumns Layout.minimumWidth: 1000 / Theme.pixelRatio Layout.fillHeight: true - /*source: visible + source: visible ? "../../components/music/WideAlbumCover.qml" : "" - onLoaded: bindAlbum(item)*/ + onLoaded: bindAlbum(item) } Item {height: 1; width: Theme.horizontalPageMargin; visible: wideAlbumCover.visible; } SilicaListView { @@ -64,8 +65,8 @@ BaseDetailPage { model: collectionModel header: Loader { width: parent.width - /*source: "../../components/music/NarrowAlbumCover.qml" - onLoaded: bindAlbum(item)*/ + source: "../../components/music/NarrowAlbumCover.qml" + onLoaded: bindAlbum(item) } section { property: "parentIndexNumber" @@ -79,7 +80,7 @@ BaseDetailPage { artists: model.artists duration: model.runTimeTicks indexNumber: model.indexNumber - onClicked: window.playbackManager.playItem(model.jellyfinId) + onClicked: window.playbackManager.playItemInList(collectionModel, model.index) } VerticalScrollDecorator {} @@ -87,7 +88,7 @@ BaseDetailPage { } function bindAlbum(item) { - //item.albumArt = Qt.binding(function(){ return Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width})}) + item.albumArt = Qt.binding(function(){ return Utils.itemImageUrl(apiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width})}) item.name = Qt.binding(function(){ return itemData.name}) item.releaseYear = Qt.binding(function() { return itemData.productionYear}) item.albumArtist = Qt.binding(function() { return itemData.albumArtist}) diff --git a/sailfish/qml/pages/itemdetails/PhotoPage.qml b/sailfish/qml/pages/itemdetails/PhotoPage.qml index d7e0805..93885cf 100644 --- a/sailfish/qml/pages/itemdetails/PhotoPage.qml +++ b/sailfish/qml/pages/itemdetails/PhotoPage.qml @@ -1,7 +1,7 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "../../components" @@ -21,7 +21,7 @@ BaseDetailPage { RemoteImage { id: image - source: ApiClient.downloadUrl(itemId) + source: apiClient.downloadUrl(itemId) fillMode: Image.PreserveAspectFit anchors.fill: parent diff --git a/sailfish/qml/pages/itemdetails/SeasonPage.qml b/sailfish/qml/pages/itemdetails/SeasonPage.qml index 99c690a..4b04b61 100644 --- a/sailfish/qml/pages/itemdetails/SeasonPage.qml +++ b/sailfish/qml/pages/itemdetails/SeasonPage.qml @@ -19,19 +19,22 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import QtQuick 2.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "../.." import "../../components" import ".." BaseDetailPage { - ShowEpisodesModel { + J.ItemModel { id: episodeModel - apiClient: ApiClient - show: itemData.seriesId - seasonId: itemData.jellyfinId - fields: ["Overview"] + loader: J.ShowEpisodesLoader { + apiClient: appWindow.apiClient + seriesId: itemData.seriesId + seasonId: itemData.jellyfinId + fields: [J.ItemFields.Overview] + autoReload: itemData.jellyfinId.length > 0 + } } Connections { @@ -42,7 +45,7 @@ BaseDetailPage { SilicaListView { anchors.fill: parent contentHeight: content.height - visible: itemData.status !== JellyfinItem.Error + //visible: itemData.status !== JellyfinItem.Error header: PageHeader { title: itemData.name @@ -60,7 +63,7 @@ BaseDetailPage { } width: Constants.libraryDelegateWidth height: Constants.libraryDelegateHeight - source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.jellyfinId, model.imageTags.Primary, "Primary", {"maxHeight": height}) + source: Utils.itemModelImageUrl(apiClient.baseUrl, model.jellyfinId, model.imageTags.Primary, "Primary", {"maxHeight": height}) blurhash: model.imageBlurHashes.Primary[model.imageTags.Primary] fillMode: Image.PreserveAspectCrop clip: true @@ -157,8 +160,8 @@ BaseDetailPage { onStatusChanged: { if (status == PageStatus.Active) { //console.log(JSON.stringify(itemData)) - episodeModel.show = itemData.seriesId - episodeModel.seasonId = itemData.jellyfinId + //episodeModel.show = itemData.seriesId + //episodeModel.seasonId = itemData.jellyfinId } } } diff --git a/sailfish/qml/pages/itemdetails/SeriesPage.qml b/sailfish/qml/pages/itemdetails/SeriesPage.qml index 933dc25..0b474b4 100644 --- a/sailfish/qml/pages/itemdetails/SeriesPage.qml +++ b/sailfish/qml/pages/itemdetails/SeriesPage.qml @@ -19,7 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import QtQuick 2.6 import Sailfish.Silica 1.0 -import nl.netsoj.chris.Jellyfin 1.0 +import nl.netsoj.chris.Jellyfin 1.0 as J import "../../components" import "../.." @@ -28,7 +28,7 @@ BaseDetailPage { SilicaFlickable { anchors.fill: parent contentHeight: content.height - visible: itemData.status !== JellyfinItem.Error + //visible: itemData.status !== JellyfinItem.Error Column { id: content @@ -47,7 +47,7 @@ BaseDetailPage { RemoteImage { id: logoImage anchors.centerIn: parent - source: Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Logo") + source: Utils.itemImageUrl(apiClient.baseUrl, itemData, "Logo") } } @@ -63,11 +63,14 @@ BaseDetailPage { text: qsTr("Seasons") } - ShowSeasonsModel { + J.ItemModel { id: showSeasonsModel - apiClient: ApiClient - show: itemData.jellyfinId - onShowChanged: reload() + loader: J.ShowSeasonsLoader { + id: showSeasonLoader + apiClient: appWindow.apiClient + seriesId: itemData.jellyfinId + autoReload: itemData.jellyfinId.length > 0 + } } Connections { target: itemData @@ -84,7 +87,7 @@ BaseDetailPage { leftMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin delegate: LibraryItemDelegate { - poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.jellyfinId, model.imageTags.Primary, "Primary", {"maxHeight": height}) + poster: Utils.itemModelImageUrl(apiClient.baseUrl, model.jellyfinId, model.imageTags.Primary, "Primary", {"maxHeight": height}) blurhash: model.imageBlurHashes["Primary"][model.imageTags.Primary] title: model.name onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.jellyfinId}) diff --git a/sailfish/qml/pages/itemdetails/UnsupportedPage.qml b/sailfish/qml/pages/itemdetails/UnsupportedPage.qml index 881fcf8..67f5cfe 100644 --- a/sailfish/qml/pages/itemdetails/UnsupportedPage.qml +++ b/sailfish/qml/pages/itemdetails/UnsupportedPage.qml @@ -31,7 +31,7 @@ BaseDetailPage { enabled: true text: qsTr("Item type (%1) unsupported").arg(itemData.type) - hintText: qsTr("This is still an alpha version :)") + hintText: qsTr("Fallback page for %2 not found either\nThis is still an alpha version :)").arg(itemData.mediaType) } } } diff --git a/sailfish/qml/pages/itemdetails/VideoPage.qml b/sailfish/qml/pages/itemdetails/VideoPage.qml index d230eba..1da8c9d 100644 --- a/sailfish/qml/pages/itemdetails/VideoPage.qml +++ b/sailfish/qml/pages/itemdetails/VideoPage.qml @@ -55,7 +55,7 @@ BaseDetailPage { PlayToolbar { id: toolbar width: parent.width - imageSource: Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width}) + imageSource: Utils.itemImageUrl(apiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width}) imageAspectRatio: Constants.horizontalVideoAspectRatio imageBlurhash: itemData.imageBlurHashes["Primary"][itemData.imageTags["Primary"]] Binding on favourited { diff --git a/sailfish/qml/pages/settings/DebugPage.qml b/sailfish/qml/pages/settings/DebugPage.qml index 7717415..c5b7fe4 100644 --- a/sailfish/qml/pages/settings/DebugPage.qml +++ b/sailfish/qml/pages/settings/DebugPage.qml @@ -54,7 +54,7 @@ Page { label: qsTr("Connection state") value: { var stateText - switch(ApiClient.websocket.state) { + switch(apiClient.websocket.state) { case 0: //- Socket state stateText = qsTr("Unconnected"); @@ -85,7 +85,7 @@ Page { break; } //- Socket state: "state no (state description)" - qsTr("%1 (%2)").arg(ApiClient.websocket.state).arg(stateText) + qsTr("%1 (%2)").arg(apiClient.websocket.state).arg(stateText) } } @@ -105,7 +105,7 @@ Page { Label { id: deviceProfile color: Theme.secondaryHighlightColor - text: JSON.stringify(ApiClient.deviceProfile, null, '\t') + text: JSON.stringify(apiClient.deviceProfile, null, '\t') } HorizontalScrollDecorator {} } diff --git a/sailfish/qml/pages/setup/AddServerConnectingPage.qml b/sailfish/qml/pages/setup/AddServerConnectingPage.qml index 8c02fe0..88fea1c 100644 --- a/sailfish/qml/pages/setup/AddServerConnectingPage.qml +++ b/sailfish/qml/pages/setup/AddServerConnectingPage.qml @@ -26,27 +26,26 @@ import "../.." * Page to indicate that the application is connecting to a server. */ Page { - property string serverName - property string serverAddress - property Page firstPage - - allowedOrientations: Orientation.All - - - BusyLabel { - text: qsTr("Connecting to %1").arg(serverName) - running: true - } - - onStatusChanged: { - if (status == PageStatus.Active) { - console.log("Connecting page active"); - ApiClient.setupConnection(); - } - } - - Connections { - target: ApiClient + property string serverName + property string serverAddress + property Page firstPage + + allowedOrientations: Orientation.All + + + BusyLabel { + text: qsTr("Connecting to %1").arg(serverName) + running: true + } + + onStatusChanged: { + if (status == PageStatus.Active) { + apiClient.setupConnection(); + } + } + + Connections { + target: apiClient onConnectionSuccess: { console.log("Login success: " + loginMessage); pageStack.replace(Qt.resolvedUrl("LoginDialog.qml"), {"loginMessage": loginMessage, "firstPage": firstPage}); diff --git a/sailfish/qml/pages/setup/AddServerPage.qml b/sailfish/qml/pages/setup/AddServerPage.qml index d8810bb..415bb60 100644 --- a/sailfish/qml/pages/setup/AddServerPage.qml +++ b/sailfish/qml/pages/setup/AddServerPage.qml @@ -28,40 +28,40 @@ import "../../" * This dialog allows manual address entry or use one of the addresses discovered via UDP broadcasts. */ Dialog { - id: dialogRoot - allowedOrientations: Orientation.All - // Picks the address of the ComboBox if selected, otherwise the manual address entry - readonly property string address: serverSelect.currentItem._address - readonly property bool addressCorrect: serverSelect.currentIndex > 0 || manualAddress.acceptableInput - readonly property string serverName: serverSelect.currentItem._name + id: dialogRoot + allowedOrientations: Orientation.All + // Picks the address of the ComboBox if selected, otherwise the manual address entry + readonly property string address: serverSelect.currentItem._address + readonly property bool addressCorrect: serverSelect.currentIndex > 0 || manualAddress.acceptableInput + readonly property string serverName: serverSelect.currentItem._name readonly property bool _isSetupPage: true - - - acceptDestination: AddServerConnectingPage { - id: connectingPage - serverName: dialogRoot.serverName - serverAddress: address - firstPage: dialogRoot - } - - Column { - width: parent.width - DialogHeader { - acceptText: qsTr("Connect") - title: qsTr("Connect to Jellyfin") - } - - J.ServerDiscoveryModel { - id: serverModel - } - - - ComboBox { - id: serverSelect - label: qsTr("Server") - description: qsTr("Sailfin will try to search for Jellyfin servers on your local network automatically") - menu: ContextMenu { + + acceptDestination: AddServerConnectingPage { + id: connectingPage + serverName: dialogRoot.serverName + serverAddress: address + firstPage: dialogRoot + } + + Column { + width: parent.width + DialogHeader { + acceptText: qsTr("Connect") + title: qsTr("Connect to Jellyfin") + } + + J.ServerDiscoveryModel { + id: serverModel + } + + + ComboBox { + id: serverSelect + label: qsTr("Server") + description: qsTr("Sailfin will try to search for Jellyfin servers on your local network automatically") + + menu: ContextMenu { MenuItem { // Special values are cool, aren't they? readonly property string _address: manualAddress.text @@ -106,8 +106,8 @@ Dialog { function tryConnect() { console.log("Hi there!") - ApiClient.baseUrl = address; - //ApiClient.setupConnection() + apiClient.baseUrl = address; + //apiClient.setupConnection() //fakeTimer.start() } diff --git a/sailfish/qml/pages/setup/LoginDialog.qml b/sailfish/qml/pages/setup/LoginDialog.qml index 007bbc3..1bce9d3 100644 --- a/sailfish/qml/pages/setup/LoginDialog.qml +++ b/sailfish/qml/pages/setup/LoginDialog.qml @@ -46,14 +46,14 @@ Dialog { } onStatusChanged: { if(status == PageStatus.Active) { - ApiClient.authenticate(username.text, password.text, true) + apiClient.authenticate(username.text, password.text, true) } } Connections { - target: ApiClient + target: apiClient onAuthenticatedChanged: { - if (ApiClient.authenticated) { + if (apiClient.authenticated) { console.log("authenticated!") pageStack.replaceAbove(null, Qt.resolvedUrl("../MainPage.qml")) } @@ -70,7 +70,7 @@ Dialog { /*PublicUserModel { id: userModel - apiClient: ApiClient + apiClient: appWindow.apiClient Component.onCompleted: reload(); }*/ @@ -103,7 +103,7 @@ Dialog { model: 0 //userModel delegate: UserGridDelegate { name: model.name - image: model.primaryImageTag ? "%1/Users/%2/Images/Primary?tag=%3".arg(ApiClient.baseUrl).arg(model.jellyfinId).arg(model.primaryImageTag) : "" + image: model.primaryImageTag ? "%1/Users/%2/Images/Primary?tag=%3".arg(apiClient.baseUrl).arg(model.jellyfinId).arg(model.primaryImageTag) : "" highlighted: model.name === username.text onHighlightedChanged: { if (highlighted) { diff --git a/sailfish/qml/qmldir b/sailfish/qml/qmldir index 401c447..0b84cf2 100644 --- a/sailfish/qml/qmldir +++ b/sailfish/qml/qmldir @@ -16,5 +16,4 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA singleton Constants 1.0 Constants.qml -singleton ApiClient 1.0 ApiClient.qml Utils 1.0 Utils.js diff --git a/sailfish/src/harbour-sailfin.cpp b/sailfish/src/harbour-sailfin.cpp index b3d1d44..e592501 100644 --- a/sailfish/src/harbour-sailfin.cpp +++ b/sailfish/src/harbour-sailfin.cpp @@ -69,7 +69,7 @@ int main(int argc, char *argv[]) { if (canSanbox && !cmdParser.isSet(sandboxOption)) { qDebug() << "Restarting in sandbox mode"; QProcess::execute(QString(SANDBOX_PROGRAM), - QStringList() << "-p" << "harbour-sailfin.desktop" << "/usr/bin/harbour-sailfin"); + QStringList() << "-p" << "harbour-sailfin.desktop" << "harbour-sailfin"); return 0; }