1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-11-25 10:25:17 +00:00

Add music library page

This commit is contained in:
Chris Josten 2022-07-29 14:26:25 +02:00
parent 0c0b91dc4b
commit dc9c3ea1b8
11 changed files with 284 additions and 102 deletions

View file

@ -253,6 +253,8 @@ extern template void setRequestLimit(Loader::GetPublicUsersParams &params, int l
extern template bool setRequestStartIndex(Loader::GetPublicUsersParams &params, int offset); extern template bool setRequestStartIndex(Loader::GetPublicUsersParams &params, int offset);
extern template void setRequestLimit(Loader::GetNextUpParams &params, int limit); extern template void setRequestLimit(Loader::GetNextUpParams &params, int limit);
extern template bool setRequestStartIndex(Loader::GetNextUpParams &params, int offset); extern template bool setRequestStartIndex(Loader::GetNextUpParams &params, int offset);
extern template void setRequestLimit(Loader::GetAlbumArtistsParams &params, int limit);
extern template bool setRequestStartIndex(Loader::GetAlbumArtistsParams &params, int offset);
extern template QList<DTO::UserDto> extractRecords(const QList<DTO::UserDto> &result); extern template QList<DTO::UserDto> extractRecords(const QList<DTO::UserDto> &result);
extern template int extractTotalRecordCount(const QList<DTO::UserDto> &result); extern template int extractTotalRecordCount(const QList<DTO::UserDto> &result);

View file

@ -290,6 +290,42 @@ public:
FWDPROP(QString, seriesId, SeriesId) FWDPROP(QString, seriesId, SeriesId)
}; };
using AlbumArtistLoaderBase = AbstractUserParameterLoader<Model::Item, DTO::BaseItemDto, DTO::BaseItemDtoQueryResult, Jellyfin::Loader::GetAlbumArtistsParams>;
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. * @brief Base class for each model that works with items.

View file

@ -184,6 +184,16 @@ bool setRequestStartIndex(Loader::GetNextUpParams &params, int offset) {
return true; return true;
} }
template<>
void setRequestLimit(Loader::GetAlbumArtistsParams &params, int limit) {
params.setLimit(limit);
}
template<>
bool setRequestStartIndex(Loader::GetAlbumArtistsParams &params, int offset) {
params.setStartIndex(offset);
return true;
}
template<> template<>
QList<DTO::UserDto> extractRecords(const QList<DTO::UserDto> &result) { QList<DTO::UserDto> extractRecords(const QList<DTO::UserDto> &result) {
return result; return result;

View file

@ -77,6 +77,7 @@ void JellyfinPlugin::registerTypes(const char *uri) {
qmlRegisterType<ViewModel::ShowEpisodesLoader>(uri, 1, 0, "ShowEpisodesLoader"); qmlRegisterType<ViewModel::ShowEpisodesLoader>(uri, 1, 0, "ShowEpisodesLoader");
qmlRegisterType<ViewModel::NextUpLoader>(uri, 1, 0, "NextUpLoader"); qmlRegisterType<ViewModel::NextUpLoader>(uri, 1, 0, "NextUpLoader");
qmlRegisterType<ViewModel::PublicUsersLoader>(uri, 1, 0, "PublicUsersLoader"); qmlRegisterType<ViewModel::PublicUsersLoader>(uri, 1, 0, "PublicUsersLoader");
qmlRegisterType<ViewModel::AlbumArtistLoader>(uri, 1, 0, "AlbumArtistLoader");
// Enumerations // Enumerations
qmlRegisterUncreatableType<Jellyfin::DTO::GeneralCommandTypeClass>(uri, 1, 0, "GeneralCommandType", "Is an enum"); qmlRegisterUncreatableType<Jellyfin::DTO::GeneralCommandTypeClass>(uri, 1, 0, "GeneralCommandType", "Is an enum");

View file

@ -18,6 +18,7 @@
*/ */
#include "JellyfinQt/viewmodel/itemmodel.h" #include "JellyfinQt/viewmodel/itemmodel.h"
#include "JellyfinQt/loader/http/artists.h"
#include "JellyfinQt/loader/http/items.h" #include "JellyfinQt/loader/http/items.h"
#include "JellyfinQt/loader/http/userlibrary.h" #include "JellyfinQt/loader/http/userlibrary.h"
#include "JellyfinQt/loader/http/userviews.h" #include "JellyfinQt/loader/http/userviews.h"
@ -57,6 +58,9 @@ ShowEpisodesLoader::ShowEpisodesLoader(QObject *parent)
NextUpLoader::NextUpLoader(QObject *parent) NextUpLoader::NextUpLoader(QObject *parent)
: NextUpLoaderBase(new Jellyfin::Loader::HTTP::GetNextUpLoader(), parent) {} : NextUpLoaderBase(new Jellyfin::Loader::HTTP::GetNextUpLoader(), parent) {}
AlbumArtistLoader::AlbumArtistLoader(QObject *parent)
: AlbumArtistLoaderBase(new Jellyfin::Loader::HTTP::GetAlbumArtistsLoader(), parent) {}
ItemModel::ItemModel(QObject *parent) ItemModel::ItemModel(QObject *parent)
: ApiModel<Model::Item>(parent) { : ApiModel<Model::Item>(parent) {
connect(this, &QAbstractItemModel::rowsInserted, this, &ItemModel::onInsertItems); connect(this, &QAbstractItemModel::rowsInserted, this, &ItemModel::onInsertItems);
@ -128,7 +132,7 @@ QSharedPointer<Model::Item> ItemModel::itemAt(int index) {
void ItemModel::onInsertItems(const QModelIndex &parent, int start, int end) { void ItemModel::onInsertItems(const QModelIndex &parent, int start, int end) {
if (parent.isValid()) return; if (parent.isValid()) return;
qDebug() << "Connecting " << (end - start + 1) << "items!"; //qDebug() << "Connecting " << (end - start + 1) << "items!";
for (int i = start; i <= end; i++) { for (int i = start; i <= end; i++) {
connect(itemAt(i).data(), &Model::Item::userDataChanged, this, &ItemModel::onUserDataUpdated); connect(itemAt(i).data(), &Model::Item::userDataChanged, this, &ItemModel::onUserDataUpdated);
} }

View file

@ -26,6 +26,7 @@ set(sailfin_QML_SOURCES
qml/components/videoplayer/VideoError.qml qml/components/videoplayer/VideoError.qml
qml/components/videoplayer/VideoHud.qml qml/components/videoplayer/VideoHud.qml
qml/components/IconListItem.qml qml/components/IconListItem.qml
qml/components/ItemChildrenShowcase.qml
qml/components/JItem.qml qml/components/JItem.qml
qml/components/LibraryItemDelegate.qml qml/components/LibraryItemDelegate.qml
qml/components/MoreSection.qml qml/components/MoreSection.qml
@ -53,7 +54,8 @@ set(sailfin_QML_SOURCES
qml/pages/itemdetails/EpisodePage.qml qml/pages/itemdetails/EpisodePage.qml
qml/pages/itemdetails/FilmPage.qml qml/pages/itemdetails/FilmPage.qml
qml/pages/itemdetails/MusicAlbumPage.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/PhotoPage.qml
qml/pages/itemdetails/SeasonPage.qml qml/pages/itemdetails/SeasonPage.qml
qml/pages/itemdetails/SeriesPage.qml qml/pages/itemdetails/SeriesPage.qml

View file

@ -112,6 +112,13 @@ function getPageUrl(mediaType, itemType, isFolder) {
return Qt.resolvedUrl("pages/itemdetails/MusicAlbumPage.qml") return Qt.resolvedUrl("pages/itemdetails/MusicAlbumPage.qml")
case "photo": case "photo":
return Qt.resolvedUrl("pages/itemdetails/PhotoPage.qml") 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: default:
switch (mediaType ? mediaType.toLowerCase() : isFolder ? "folder" : "") { switch (mediaType ? mediaType.toLowerCase() : isFolder ? "folder" : "") {
case "folder": case "folder":

View file

@ -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);
}
}
}
}

View file

@ -78,73 +78,40 @@ Page {
} }
} }
MoreSection { ItemChildrenShowcase {
//- Section header for films and TV shows that an user hasn't completed yet. //- Section header for films and TV shows that an user hasn't completed yet.
text: qsTr("Resume watching") text: qsTr("Resume watching")
clickable: false clickable: false
busy: userResumeLoader.status === J.ModelStatus.Loading loader: J.ResumeItemsLoader {
Loader { id: userResumeLoader
width: parent.width apiClient: appWindow.apiClient
sourceComponent: carrouselView limit: 12
property alias itemModel: userResumeModel //recursive: true
property string collectionType: "series"
J.ItemModel {
id: userResumeModel
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. //- Section header for next episodes in a TV show that an user was watching.
text: qsTr("Next up") text: qsTr("Next up")
clickable: false clickable: false
busy: showNextUpLoader.status === J.ModelStatus.Loading loader: J.NextUpLoader {
id: showNextUpLoader
Loader { apiClient: appWindow.apiClient
width: parent.width enableUserData: true
sourceComponent: carrouselView
property alias itemModel: showNextUpModel
property string collectionType: "series"
J.ItemModel {
id: showNextUpModel
loader: J.NextUpLoader {
id: showNextUpLoader
apiClient: appWindow.apiClient
enableUserData: true
}
}
} }
} }
Repeater { Repeater {
model: mediaLibraryModel model: mediaLibraryModel
MoreSection { ItemChildrenShowcase {
text: model.name text: model.name
busy: userItemModel.status !== J.UsersViewsLoader.Ready onHeaderClicked: appWindow.navigateToItem(model.jellyfinId, model.collectionType, model.type, model.isFolder);
onHeaderClicked: appWindow.navigateToItem(model.jellyfinId, model.mediaType, model.type, model.isFolder); collectionType: model.collectionType || ""
Loader { loader: J.LatestMediaLoader {
width: parent.width apiClient: appWindow.apiClient
sourceComponent: carrouselView parentId: jellyfinId
property alias itemModel: userItemModel }
property string collectionType: model.collectionType || "" Connections {
target: mediaLibraryLoader
J.ItemModel { onReady: loader.reload()
id: userItemModel
loader: J.LatestMediaLoader {
apiClient: appWindow.apiClient
parentId: jellyfinId
}
}
Connections {
target: mediaLibraryLoader
onReady: userItemModel.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" state: "default"
states: [ states: [
State { State {

View file

@ -73,10 +73,9 @@ BaseDetailPage {
} }
PullDownMenu { PullDownMenu {
id: downMenu id: downMenu
visible: visibleChildren.length > 0 visible: pageRoot.allowSort
MenuItem { MenuItem {
id: sortMenuItem id: sortMenuItem
visible: pageRoot.allowSort
//: Menu item for selecting the sort order of a collection //: Menu item for selecting the sort order of a collection
text: qsTr("Sort by") text: qsTr("Sort by")
onClicked: pageStack.push(sortPageComponent) onClicked: pageStack.push(sortPageComponent)

View file

@ -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")
})
}
}
}
}