diff --git a/core/include/JellyfinQt/apimodel.h b/core/include/JellyfinQt/apimodel.h index f1aa95e..b4860c4 100644 --- a/core/include/JellyfinQt/apimodel.h +++ b/core/include/JellyfinQt/apimodel.h @@ -253,6 +253,8 @@ extern template void setRequestLimit(Loader::GetPublicUsersParams ¶ms, int l extern template bool setRequestStartIndex(Loader::GetPublicUsersParams ¶ms, int offset); extern template void setRequestLimit(Loader::GetNextUpParams ¶ms, int limit); extern template bool setRequestStartIndex(Loader::GetNextUpParams ¶ms, int offset); +extern template void setRequestLimit(Loader::GetAlbumArtistsParams ¶ms, int limit); +extern template bool setRequestStartIndex(Loader::GetAlbumArtistsParams ¶ms, int offset); extern template QList extractRecords(const QList &result); extern template int extractTotalRecordCount(const QList &result); diff --git a/core/include/JellyfinQt/viewmodel/itemmodel.h b/core/include/JellyfinQt/viewmodel/itemmodel.h index b3385bf..8024770 100644 --- a/core/include/JellyfinQt/viewmodel/itemmodel.h +++ b/core/include/JellyfinQt/viewmodel/itemmodel.h @@ -290,6 +290,42 @@ public: FWDPROP(QString, seriesId, SeriesId) }; +using AlbumArtistLoaderBase = AbstractUserParameterLoader; +class AlbumArtistLoader : public AlbumArtistLoaderBase { + Q_OBJECT +public: + explicit AlbumArtistLoader(QObject *parent = nullptr); + + FWDLISTPROP(Jellyfin::DTO::ImageTypeClass::Value, enableImageTypes, EnableImageTypes); + FWDPROP(bool, enableImages, EnableImages) + FWDPROP(bool, enableTotalRecordCount, EnableTotalRecordCount) + FWDPROP(bool, enableUserData, EnableUserData) + FWDPROP(QStringList, excludeItemTypes, ExcludeItemTypes) + FWDLISTPROP(Jellyfin::DTO::ItemFieldsClass::Value, fields, Fields) + FWDLISTPROP(Jellyfin::DTO::ItemFilterClass::Value, filters, Filters) + FWDPROP(QStringList, genreIds, GenreIds) + FWDPROP(QStringList, genres, Genres) + FWDPROP(qint32, imageTypeLimit, ImageTypeLimit) + FWDPROP(QStringList, includeItemTypes, IncludeItemTypes) + FWDPROP(bool, isFavorite, IsFavorite) + FWDPROP(int, limit, Limit) + FWDPROP(QStringList, mediaTypes, MediaTypes) + FWDPROP(double, minCommunityRating, MinCommunityRating) + FWDPROP(QString, nameLessThan, NameLessThan) + FWDPROP(QString, nameStartsWith, NameStartsWith) + FWDPROP(QString, nameStartsWithOrGreater, NameStartsWithOrGreater) + FWDPROP(QStringList, officialRatings, OfficialRatings) + FWDPROP(QString, parentId, ParentId) + FWDPROP(QStringList, personIds, PersonIds) + FWDPROP(QStringList, personTypes, PersonTypes) + FWDPROP(QString, searchTerm, SearchTerm) + FWDPROP(int, startIndex, StartIndex) + FWDPROP(QStringList, studioIds, StudioIds) + FWDPROP(QStringList, studios, Studios) + FWDPROP(QStringList, tags, Tags) + FWDPROP(QString, userId, UserId) + FWDLISTPROP(int, years, Years); +}; /** * @brief Base class for each model that works with items. diff --git a/core/src/apimodel.cpp b/core/src/apimodel.cpp index 739642c..ed6d90e 100644 --- a/core/src/apimodel.cpp +++ b/core/src/apimodel.cpp @@ -184,6 +184,16 @@ bool setRequestStartIndex(Loader::GetNextUpParams ¶ms, int offset) { return true; } +template<> +void setRequestLimit(Loader::GetAlbumArtistsParams ¶ms, int limit) { + params.setLimit(limit); +} +template<> +bool setRequestStartIndex(Loader::GetAlbumArtistsParams ¶ms, int offset) { + params.setStartIndex(offset); + return true; +} + template<> QList extractRecords(const QList &result) { return result; diff --git a/core/src/jellyfin.cpp b/core/src/jellyfin.cpp index 3d95be2..609ffc9 100644 --- a/core/src/jellyfin.cpp +++ b/core/src/jellyfin.cpp @@ -77,6 +77,7 @@ void JellyfinPlugin::registerTypes(const char *uri) { qmlRegisterType(uri, 1, 0, "ShowEpisodesLoader"); qmlRegisterType(uri, 1, 0, "NextUpLoader"); qmlRegisterType(uri, 1, 0, "PublicUsersLoader"); + qmlRegisterType(uri, 1, 0, "AlbumArtistLoader"); // Enumerations qmlRegisterUncreatableType(uri, 1, 0, "GeneralCommandType", "Is an enum"); diff --git a/core/src/viewmodel/itemmodel.cpp b/core/src/viewmodel/itemmodel.cpp index f94f074..cf4907e 100644 --- a/core/src/viewmodel/itemmodel.cpp +++ b/core/src/viewmodel/itemmodel.cpp @@ -18,6 +18,7 @@ */ #include "JellyfinQt/viewmodel/itemmodel.h" +#include "JellyfinQt/loader/http/artists.h" #include "JellyfinQt/loader/http/items.h" #include "JellyfinQt/loader/http/userlibrary.h" #include "JellyfinQt/loader/http/userviews.h" @@ -57,6 +58,9 @@ ShowEpisodesLoader::ShowEpisodesLoader(QObject *parent) NextUpLoader::NextUpLoader(QObject *parent) : NextUpLoaderBase(new Jellyfin::Loader::HTTP::GetNextUpLoader(), parent) {} +AlbumArtistLoader::AlbumArtistLoader(QObject *parent) + : AlbumArtistLoaderBase(new Jellyfin::Loader::HTTP::GetAlbumArtistsLoader(), parent) {} + ItemModel::ItemModel(QObject *parent) : ApiModel(parent) { connect(this, &QAbstractItemModel::rowsInserted, this, &ItemModel::onInsertItems); @@ -128,7 +132,7 @@ QSharedPointer ItemModel::itemAt(int index) { void ItemModel::onInsertItems(const QModelIndex &parent, int start, int end) { if (parent.isValid()) return; - qDebug() << "Connecting " << (end - start + 1) << "items!"; + //qDebug() << "Connecting " << (end - start + 1) << "items!"; for (int i = start; i <= end; i++) { connect(itemAt(i).data(), &Model::Item::userDataChanged, this, &ItemModel::onUserDataUpdated); } diff --git a/sailfish/CMakeLists.txt b/sailfish/CMakeLists.txt index 85745e6..5018548 100644 --- a/sailfish/CMakeLists.txt +++ b/sailfish/CMakeLists.txt @@ -26,6 +26,7 @@ set(sailfin_QML_SOURCES qml/components/videoplayer/VideoError.qml qml/components/videoplayer/VideoHud.qml qml/components/IconListItem.qml + qml/components/ItemChildrenShowcase.qml qml/components/JItem.qml qml/components/LibraryItemDelegate.qml qml/components/MoreSection.qml @@ -53,7 +54,8 @@ set(sailfin_QML_SOURCES qml/pages/itemdetails/EpisodePage.qml qml/pages/itemdetails/FilmPage.qml qml/pages/itemdetails/MusicAlbumPage.qml - qml/pages/itemdetails/MusicArtistPage.qml + qml/pages/itemdetails/MusicArtistPage.qml + qml/pages/itemdetails/MusicLibraryPage.qml qml/pages/itemdetails/PhotoPage.qml qml/pages/itemdetails/SeasonPage.qml qml/pages/itemdetails/SeriesPage.qml diff --git a/sailfish/qml/Utils.js b/sailfish/qml/Utils.js index 1d4b183..049c5f5 100644 --- a/sailfish/qml/Utils.js +++ b/sailfish/qml/Utils.js @@ -112,6 +112,13 @@ function getPageUrl(mediaType, itemType, isFolder) { return Qt.resolvedUrl("pages/itemdetails/MusicAlbumPage.qml") case "photo": return Qt.resolvedUrl("pages/itemdetails/PhotoPage.qml") + case "collectionfolder": + // TODO: support for other collection folders + switch(mediaType.toLowerCase()) { + case "music": + return Qt.resolvedUrl("pages/itemdetails/MusicLibraryPage.qml") + } + // FALLTRHOUGH default: switch (mediaType ? mediaType.toLowerCase() : isFolder ? "folder" : "") { case "folder": diff --git a/sailfish/qml/components/ItemChildrenShowcase.qml b/sailfish/qml/components/ItemChildrenShowcase.qml new file mode 100644 index 0000000..93084e5 --- /dev/null +++ b/sailfish/qml/components/ItemChildrenShowcase.qml @@ -0,0 +1,60 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + +import nl.netsoj.chris.Jellyfin 1.0 as J + +import "../" + +MoreSection { + id: header + busy: itemModel.loader.status === J.ModelStatus.Loading || extraBusy + property bool extraBusy: false + property alias loader: itemModel.loader + property string collectionType + property bool collapseWhenEmpty: true + + J.ItemModel { + id: itemModel + } + + SilicaListView { + readonly property bool isPortrait: Utils.usePortraitCover(collectionType) + id: list + clip: true + height: { + if (count > 0 || !collapseWhenEmpty) { + if (isPortrait) { + Constants.libraryDelegatePosterHeight + } else { + Constants.libraryDelegateHeight + } + } else { + 0 + } + } + Behavior on height { + NumberAnimation { easing.type: Easing.OutQuad; duration: 300 } + } + model: itemModel + width: parent.width + orientation: ListView.Horizontal + leftMargin: Theme.horizontalPageMargin + rightMargin: Theme.horizontalPageMargin + spacing: Theme.paddingLarge + delegate: LibraryItemDelegate { + property string id: model.jellyfinId + title: model.name + poster: Utils.itemModelImageUrl(appWindow.apiClient.baseUrl, model.jellyfinId, model.imageTags["Primary"], "Primary", {"height": height}) + Binding on blurhash { + when: poster != "" + value: model.imageBlurHashes["Primary"][model.imageTags["Primary"]] + } + landscape: !list.isPortrait + progress: (typeof model.userDataPlayedProgress !== 0.0) ? model.userDataPlayedPercentage / 100 : 0.0 + + onClicked: { + appWindow.navigateToItem(model.jellyfinId, model.mediaType, model.type, model.isFolder); + } + } + } +} diff --git a/sailfish/qml/pages/MainPage.qml b/sailfish/qml/pages/MainPage.qml index d279af0..e894d75 100644 --- a/sailfish/qml/pages/MainPage.qml +++ b/sailfish/qml/pages/MainPage.qml @@ -78,73 +78,40 @@ Page { } } - MoreSection { + ItemChildrenShowcase { //- Section header for films and TV shows that an user hasn't completed yet. text: qsTr("Resume watching") clickable: false - busy: userResumeLoader.status === J.ModelStatus.Loading - Loader { - width: parent.width - sourceComponent: carrouselView - property alias itemModel: userResumeModel - property string collectionType: "series" - - J.ItemModel { - id: userResumeModel - loader: J.ResumeItemsLoader { - id: userResumeLoader - apiClient: appWindow.apiClient - limit: 12 - //recursive: true - } - } + loader: J.ResumeItemsLoader { + id: userResumeLoader + apiClient: appWindow.apiClient + limit: 12 + //recursive: true } } - MoreSection { + ItemChildrenShowcase { //- Section header for next episodes in a TV show that an user was watching. text: qsTr("Next up") clickable: false - busy: showNextUpLoader.status === J.ModelStatus.Loading - - Loader { - width: parent.width - sourceComponent: carrouselView - property alias itemModel: showNextUpModel - property string collectionType: "series" - - J.ItemModel { - id: showNextUpModel - loader: J.NextUpLoader { - id: showNextUpLoader - apiClient: appWindow.apiClient - enableUserData: true - } - } + loader: J.NextUpLoader { + id: showNextUpLoader + apiClient: appWindow.apiClient + enableUserData: true } } Repeater { model: mediaLibraryModel - MoreSection { + ItemChildrenShowcase { text: model.name - busy: userItemModel.status !== J.UsersViewsLoader.Ready - onHeaderClicked: appWindow.navigateToItem(model.jellyfinId, model.mediaType, model.type, model.isFolder); - Loader { - width: parent.width - sourceComponent: carrouselView - property alias itemModel: userItemModel - property string collectionType: model.collectionType || "" - - J.ItemModel { - id: userItemModel - loader: J.LatestMediaLoader { - apiClient: appWindow.apiClient - parentId: jellyfinId - } - } - Connections { - target: mediaLibraryLoader - onReady: userItemModel.reload() - } + onHeaderClicked: appWindow.navigateToItem(model.jellyfinId, model.collectionType, model.type, model.isFolder); + collectionType: model.collectionType || "" + loader: J.LatestMediaLoader { + apiClient: appWindow.apiClient + parentId: jellyfinId + } + Connections { + target: mediaLibraryLoader + onReady: loader.reload() } } } @@ -194,50 +161,6 @@ Page { } } - Component { - id: carrouselView - SilicaListView { - property bool isPortrait: Utils.usePortraitCover(collectionType) - id: list - clip: true - height: { - if (count > 0) { - if (isPortrait) { - Constants.libraryDelegatePosterHeight - } else { - Constants.libraryDelegateHeight - } - } else { - 0 - } - } - Behavior on height { - NumberAnimation { easing.type: Easing.OutQuad; duration: 300 } - } - model: itemModel - width: parent.width - orientation: ListView.Horizontal - leftMargin: Theme.horizontalPageMargin - rightMargin: Theme.horizontalPageMargin - spacing: Theme.paddingLarge - delegate: LibraryItemDelegate { - property string id: model.jellyfinId - title: model.name - poster: Utils.itemModelImageUrl(appWindow.apiClient.baseUrl, model.jellyfinId, model.imageTags["Primary"], "Primary", {"height": height}) - Binding on blurhash { - when: poster !== "" - value: model.imageBlurHashes["Primary"][model.imageTags["Primary"]] - } - landscape: !isPortrait - progress: (typeof model.userDataPlayedProgress !== 0.0) ? model.userDataPlayedPercentage / 100 : 0.0 - - onClicked: { - appWindow.navigateToItem(model.jellyfinId, model.mediaType, model.type, model.isFolder); - } - } - } - } - state: "default" states: [ State { diff --git a/sailfish/qml/pages/itemdetails/CollectionPage.qml b/sailfish/qml/pages/itemdetails/CollectionPage.qml index ffd6c2b..8723095 100644 --- a/sailfish/qml/pages/itemdetails/CollectionPage.qml +++ b/sailfish/qml/pages/itemdetails/CollectionPage.qml @@ -73,10 +73,9 @@ BaseDetailPage { } PullDownMenu { id: downMenu - visible: visibleChildren.length > 0 + visible: pageRoot.allowSort MenuItem { id: sortMenuItem - visible: pageRoot.allowSort //: Menu item for selecting the sort order of a collection text: qsTr("Sort by") onClicked: pageStack.push(sortPageComponent) diff --git a/sailfish/qml/pages/itemdetails/MusicLibraryPage.qml b/sailfish/qml/pages/itemdetails/MusicLibraryPage.qml new file mode 100644 index 0000000..2dda4f1 --- /dev/null +++ b/sailfish/qml/pages/itemdetails/MusicLibraryPage.qml @@ -0,0 +1,138 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + +import nl.netsoj.chris.Jellyfin 1.0 as J + +import "../../components" +import "../.." + +BaseDetailPage { + id: musicLibraryPage + property bool _firstTimeLoaded: false + + onStatusChanged: { + if (status == PageStatus.Active) { + _firstTimeLoaded = true + } + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: content.height + + Component { + id: albumArtistLoaderComponent + J.AlbumArtistLoader { + apiClient: appWindow.apiClient + parentId: itemData.jellyfinId + autoReload: false + } + } + Component { + id: albumLoaderComponent + J.UserItemsLoader { + apiClient: appWindow.apiClient + parentId: itemData.jellyfinId + includeItemTypes: "MusicAlbum" + recursive: true + sortBy: "SortName" + autoReload: false + } + } + Component { + id: playlistLoaderComponent + J.UserItemsLoader { + apiClient: appWindow.apiClient + parentId: itemData.jellyfinId + includeItemTypes: "Playlist" + recursive: true + sortBy: "SortName" + autoReload: false + } + } + + Column { + id: content + width: parent.width + + PageHeader { + title: itemData.name + } + + ItemChildrenShowcase { + //: Header on music library: Recently added music albums + text: qsTr("Recently added") + //collapseWhenEmpty: false + extraBusy: !_firstTimeLoaded + clickable: false + loader: J.LatestMediaLoader { + apiClient: appWindow.apiClient + parentId: itemData.jellyfinId + autoReload: _firstTimeLoaded && itemData.jellyfinId.length > 0 + includeItemTypes: "Audio" + limit: 12 + } + + } + + ItemChildrenShowcase { + text: qsTr("Albums") + //collapseWhenEmpty: false + extraBusy: !_firstTimeLoaded + loader: J.UserItemsLoader { + apiClient: appWindow.apiClient + parentId: itemData.jellyfinId + includeItemTypes: "MusicAlbum" + autoReload: _firstTimeLoaded && itemData.jellyfinId.length > 0 + sortBy: "Random" + recursive: true + limit: 12 + } + onHeaderClicked: pageStack.push(Qt.resolvedUrl("CollectionPage.qml"), { + "loader": albumLoaderComponent.createObject(musicLibraryPage), + //: Page title for the list of all albums within the music library + "pageTitle": qsTr("Albums") + }) + } + + ItemChildrenShowcase { + text: qsTr("Playlists") + //collapseWhenEmpty: false + extraBusy: !_firstTimeLoaded + loader: J.UserItemsLoader { + apiClient: appWindow.apiClient + parentId: itemData.jellyfinId + includeItemTypes: "Playlist" + autoReload: _firstTimeLoaded && itemData.jellyfinId.length > 0 + sortBy: "Random" + recursive: true + limit: 12 + } + onHeaderClicked: pageStack.push(Qt.resolvedUrl("CollectionPage.qml"), { + "loader": playlistLoaderComponent.createObject(musicLibraryPage), + //: Page title for the list of all playlists within the music library + "pageTitle": qsTr("Playlists") + }) + } + + ItemChildrenShowcase { + //: Header for music artists + text: qsTr("Artists") + //collapseWhenEmpty: false + extraBusy: !_firstTimeLoaded + loader: J.AlbumArtistLoader { + apiClient: appWindow.apiClient + parentId: itemData.jellyfinId + autoReload: _firstTimeLoaded && itemData.jellyfinId.length > 0 + limit: 12 + } + onHeaderClicked: pageStack.push(Qt.resolvedUrl("CollectionPage.qml"), { + "loader": albumArtistLoaderComponent.createObject(musicLibraryPage), + "allowSort": false, + //: Page title for the list of all artists within the music library + "pageTitle": qsTr("Artists") + }) + } + } + } +}