diff --git a/harbour-sailfin.pro b/harbour-sailfin.pro index a665637..b80989c 100644 --- a/harbour-sailfin.pro +++ b/harbour-sailfin.pro @@ -67,6 +67,7 @@ DISTFILES += \ qml/pages/itemdetails/SeasonPage.qml \ qml/pages/itemdetails/SeriesPage.qml \ qml/pages/itemdetails/UnsupportedPage.qml \ + qml/pages/itemdetails/VideoPage.qml \ qml/pages/setup/AddServerConnectingPage.qml \ qml/pages/setup/LoginDialog.qml \ qml/qmldir diff --git a/qml/Constants.qml b/qml/Constants.qml index b69e2bb..a7d1a8b 100644 --- a/qml/Constants.qml +++ b/qml/Constants.qml @@ -27,5 +27,8 @@ QtObject { readonly property real libraryDelegateHeight: Screen.width / 3 readonly property real libraryDelegatePosterHeight: Screen.width / 2 + readonly property real libraryProgressHeight: Theme.paddingMedium + + readonly property real horizontalVideoAspectRatio: 1.66666 //itemData.PrimaryImageAspectRatio } diff --git a/qml/Utils.js b/qml/Utils.js index efbe310..f54c19a 100644 --- a/qml/Utils.js +++ b/qml/Utils.js @@ -60,19 +60,26 @@ function usePortraitCover(itemType) { /** * Returns the page url for a certain item type. */ -function getPageUrl(itemType) { +function getPageUrl(mediaType, itemType) { switch (itemType.toLowerCase()) { case "series": return Qt.resolvedUrl("pages/itemdetails/SeriesPage.qml") case "movie": return Qt.resolvedUrl("pages/itemdetails/FilmPage.qml") case "collection": - return Qt.resolvedUrl("pages/itemdetails/ColectionPage.qml") + return Qt.resolvedUrl("pages/itemdetails/CollectionPage.qml") case "season": return Qt.resolvedUrl("pages/itemdetails/SeasonPage.qml") case "episode": return Qt.resolvedUrl("pages/itemdetails/EpisodePage.qml") default: - return Qt.resolvedUrl("pages/itemdetails/UnsupportedPage.qml") + switch (mediaType ? mediaType.toLowerCase() : "folder") { + case "folder": + return Qt.resolvedUrl("pages/itemdetails/CollectionPage.qml") + case "video": + return Qt.resolvedUrl("pages/itemdetails/VideoPage.qml") + default: + return Qt.resolvedUrl("pages/itemdetails/UnsupportedPage.qml") + } } } diff --git a/qml/pages/MainPage.qml b/qml/pages/MainPage.qml index afd82ab..dc3bf6c 100644 --- a/qml/pages/MainPage.qml +++ b/qml/pages/MainPage.qml @@ -202,7 +202,7 @@ Page { progress: model.userData.PlayedPercentage / 100 onClicked: { - pageStack.push(Utils.getPageUrl(model.type), {"itemId": model.id}) + pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.id}) } } } diff --git a/qml/pages/itemdetails/CollectionPage.qml b/qml/pages/itemdetails/CollectionPage.qml index 50d82e1..e8afc10 100644 --- a/qml/pages/itemdetails/CollectionPage.qml +++ b/qml/pages/itemdetails/CollectionPage.qml @@ -62,17 +62,13 @@ BaseDetailPage { fillMode: Image.PreserveAspectCrop clip: true } - Rectangle { + Shim { anchors { left: parent.left bottom: parent.bottom right: parent.right } height: itemName.height + Theme.paddingSmall * 2 - gradient: Gradient { - GradientStop { position: 0.0; color: "transparent" } - GradientStop { position: 1.0; color: Theme.highlightDimmerColor } - } visible: itemImage.status !== Image.Null } Label { @@ -96,7 +92,7 @@ BaseDetailPage { pageStack.push(Qt.resolvedUrl("CollectionPage.qml"), {"itemId": model.id}) break; default: - pageStack.push(Utils.getPageUrl(model.type), {"itemId": model.id}) + pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.id}) } } } @@ -110,6 +106,8 @@ BaseDetailPage { VerticalScrollDecorator {} } + // The page for selecting a sort order + Component { id: sortPageComponent Page { @@ -139,8 +137,23 @@ BaseDetailPage { } text: model.name } - onClicked: { - collectionModel.sortBy = [model.value] + menu: ContextMenu { + MenuItem { + //: Sort order + text: qsTr("Ascending") + onClicked: apply(model.value, ApiModel.Ascending) + } + MenuItem { + //: Sort order + text: qsTr("Descending") + onClicked: apply(model.value, ApiModel.Descending) + } + } + onClicked: openMenu() + + function apply(field, order) { + collectionModel.sortBy = [field]; + collectionModel.sortOrder = order; collectionModel.reload() pageStack.pop() } diff --git a/qml/pages/itemdetails/SeasonPage.qml b/qml/pages/itemdetails/SeasonPage.qml index e983c2d..64f2214 100644 --- a/qml/pages/itemdetails/SeasonPage.qml +++ b/qml/pages/itemdetails/SeasonPage.qml @@ -122,7 +122,7 @@ BaseDetailPage { wrapMode: Text.WordWrap elide: Text.ElideRight } - onClicked: pageStack.push(Utils.getPageUrl(model.type), {"itemId": model.id}) + onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.id}) } } diff --git a/qml/pages/itemdetails/SeriesPage.qml b/qml/pages/itemdetails/SeriesPage.qml index be5c45d..8e6617c 100644 --- a/qml/pages/itemdetails/SeriesPage.qml +++ b/qml/pages/itemdetails/SeriesPage.qml @@ -80,7 +80,7 @@ BaseDetailPage { delegate: LibraryItemDelegate { poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags["Primary"], "Primary", {"maxHeight": height}) title: model.name - onClicked: pageStack.push(Utils.getPageUrl(model.type), {"itemId": model.id}) + onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.id}) } } diff --git a/qml/pages/itemdetails/VideoPage.qml b/qml/pages/itemdetails/VideoPage.qml new file mode 100644 index 0000000..59b38cd --- /dev/null +++ b/qml/pages/itemdetails/VideoPage.qml @@ -0,0 +1,63 @@ +/* +Sailfin: a Jellyfin client written using Qt +Copyright (C) 2020 Chris Josten + +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 +*/ + +import QtQuick 2.6 +import Sailfish.Silica 1.0 + +import nl.netsoj.chris.Jellyfin 1.0 + +import "../../components" +import "../.." + +/** + * Fallback page for everything that's a video, but we haven't a more specific page for, like + * the FilmPage or EpisodePage. + */ +BaseDetailPage { + SilicaFlickable { + anchors.fill: parent + contentHeight: content.height + + Column { + id: content + width: parent.width + spacing: Theme.paddingMedium + + PageHeader { + title: itemData.Name + description: qsTr("Run time: %2").arg(Utils.ticksToText(itemData.RunTimeTicks)) + } + + PlayToolbar { + width: parent.width + imageSource: Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width}) + imageAspectRatio: Constants.horizontalVideoAspectRatio + onPlayPressed: pageStack.push(Qt.resolvedUrl("../../pages/VideoPage.qml"), + {"itemId": itemId, "itemData": itemData, "audioTrack": trackSelector.audioTrack, + "subtitleTrack": trackSelector.subtitleTrack }) + } + + VideoTrackSelector { + id: trackSelector + width: parent.width + tracks: itemData.MediaStreams + } + } + } +} diff --git a/src/jellyfinapimodel.cpp b/src/jellyfinapimodel.cpp index 0fa9479..f32a07a 100644 --- a/src/jellyfinapimodel.cpp +++ b/src/jellyfinapimodel.cpp @@ -60,6 +60,9 @@ void ApiModel::load(LoadType type) { if (!m_sortBy.empty()) { query.addQueryItem("SortBy", m_sortBy.join(",")); } + if (m_sortOrder != Unspecified) { + query.addQueryItem("SortOrder", m_sortOrder == Ascending ? "Ascending" : "Descending"); + } if (!m_imageTypes.empty()) { query.addQueryItem("ImageTypes", m_imageTypes.join(",")); } @@ -208,7 +211,7 @@ void ApiModel::addQueryParameters(QUrlQuery &query) { Q_UNUSED(query)} void registerModels(const char *URI) { qmlRegisterUncreatableType(URI, 1, 0, "ApiModel", "Is enum and base class"); - qmlRegisterUncreatableType(URI, 1, 0, "SortOrder", "Is enum"); + qmlRegisterUncreatableType(URI, 1, 0, "SortOptions", "Is enum"); qmlRegisterType(URI, 1, 0, "PublicUserModel"); qmlRegisterType(URI, 1, 0, "UserViewModel"); qmlRegisterType(URI, 1, 0, "UserItemModel"); diff --git a/src/jellyfinapimodel.h b/src/jellyfinapimodel.h index a9f6b9f..ab6136d 100644 --- a/src/jellyfinapimodel.h +++ b/src/jellyfinapimodel.h @@ -31,9 +31,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include "jellyfinapiclient.h" namespace Jellyfin { -class SortOrder { - Q_GADGET +class SortOptions : public QObject{ + Q_OBJECT public: + explicit SortOptions (QObject *parent = nullptr) : QObject(parent) {} enum SortBy { Album, AlbumArtist, @@ -54,6 +55,7 @@ public: Q_ENUM(SortBy) }; + /** * @brief Abstract model for displaying a REST JSON collection. Role names will be based on the fields encountered in the * first record. @@ -90,6 +92,13 @@ public: }; Q_ENUM(ModelStatus) + enum SortOrder { + Unspecified, + Ascending, + Descending + }; + Q_ENUM(SortOrder) + /** * @brief Creates a new basemodel * @param path The path (relative to the baseUrl of JellyfinApiClient) to make the call to. @@ -130,6 +139,7 @@ public: Q_PROPERTY(QString seasonId MEMBER m_seasonId NOTIFY seasonIdChanged) Q_PROPERTY(QList imageTypes MEMBER m_imageTypes NOTIFY imageTypesChanged) Q_PROPERTY(bool recursive MEMBER m_recursive) + Q_PROPERTY(SortOrder sortOrder MEMBER m_sortOrder NOTIFY sortOrderChanged) // Path properties Q_PROPERTY(QString show MEMBER m_show NOTIFY showChanged) @@ -164,6 +174,7 @@ signals: void limitChanged(int newLimit); void parentIdChanged(QString newParentId); void sortByChanged(QList newSortOrder); + void sortOrderChanged(SortOrder newSortOrder); void showChanged(QString newShow); void seasonIdChanged(QString newSeasonId); void fieldsChanged(QList newFields); @@ -214,6 +225,7 @@ protected: QList m_fields; QList m_imageTypes; QList m_sortBy = {}; + SortOrder m_sortOrder = Unspecified; bool m_recursive = false; QHash m_roles; @@ -228,7 +240,7 @@ private: * @brief Generates roleNames based on the first record in m_array. */ void generateFields(); - QString sortByToString(SortOrder::SortBy sortBy); + QString sortByToString(SortOptions::SortBy sortBy); }; /** diff --git a/translations/harbour-sailfin.ts b/translations/harbour-sailfin.ts index 56ee869..34c9d59 100644 --- a/translations/harbour-sailfin.ts +++ b/translations/harbour-sailfin.ts @@ -93,6 +93,16 @@ Date added + + Ascending + Sort order + + + + Descending + Sort order + + CoverPage @@ -104,11 +114,15 @@ EpisodePage - Episode %1–%2 Season %3 + Episode %1–%2 | %3 - Episode %1 Season %2 + Episode %1 | %2 + + + + Overview @@ -118,6 +132,10 @@ Released: %1 — Run time: %2 + + Overview + + LegalPage @@ -301,6 +319,13 @@ + + VideoPage + + Run time: %2 + + + VideoTrackSelector