mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-11-22 01:05:17 +00:00
Added SortOrder and fallback pages.
Once again I couldn't stop myself from being sidetracked. [Collections]: Added: allow specifying sort order (Ascending, Descending) [General]: Improved: Added video fallback page to allow unknown video types to be played, although without extra metadata. [General]: Improved: Added folder fallback, so unknown collection types are at least displayed without metadata.
This commit is contained in:
parent
b68da318f2
commit
5057867ade
|
@ -67,6 +67,7 @@ DISTFILES += \
|
||||||
qml/pages/itemdetails/SeasonPage.qml \
|
qml/pages/itemdetails/SeasonPage.qml \
|
||||||
qml/pages/itemdetails/SeriesPage.qml \
|
qml/pages/itemdetails/SeriesPage.qml \
|
||||||
qml/pages/itemdetails/UnsupportedPage.qml \
|
qml/pages/itemdetails/UnsupportedPage.qml \
|
||||||
|
qml/pages/itemdetails/VideoPage.qml \
|
||||||
qml/pages/setup/AddServerConnectingPage.qml \
|
qml/pages/setup/AddServerConnectingPage.qml \
|
||||||
qml/pages/setup/LoginDialog.qml \
|
qml/pages/setup/LoginDialog.qml \
|
||||||
qml/qmldir
|
qml/qmldir
|
||||||
|
|
|
@ -27,5 +27,8 @@ QtObject {
|
||||||
readonly property real libraryDelegateHeight: Screen.width / 3
|
readonly property real libraryDelegateHeight: Screen.width / 3
|
||||||
|
|
||||||
readonly property real libraryDelegatePosterHeight: Screen.width / 2
|
readonly property real libraryDelegatePosterHeight: Screen.width / 2
|
||||||
|
|
||||||
readonly property real libraryProgressHeight: Theme.paddingMedium
|
readonly property real libraryProgressHeight: Theme.paddingMedium
|
||||||
|
|
||||||
|
readonly property real horizontalVideoAspectRatio: 1.66666 //itemData.PrimaryImageAspectRatio
|
||||||
}
|
}
|
||||||
|
|
13
qml/Utils.js
13
qml/Utils.js
|
@ -60,19 +60,26 @@ function usePortraitCover(itemType) {
|
||||||
/**
|
/**
|
||||||
* Returns the page url for a certain item type.
|
* Returns the page url for a certain item type.
|
||||||
*/
|
*/
|
||||||
function getPageUrl(itemType) {
|
function getPageUrl(mediaType, itemType) {
|
||||||
switch (itemType.toLowerCase()) {
|
switch (itemType.toLowerCase()) {
|
||||||
case "series":
|
case "series":
|
||||||
return Qt.resolvedUrl("pages/itemdetails/SeriesPage.qml")
|
return Qt.resolvedUrl("pages/itemdetails/SeriesPage.qml")
|
||||||
case "movie":
|
case "movie":
|
||||||
return Qt.resolvedUrl("pages/itemdetails/FilmPage.qml")
|
return Qt.resolvedUrl("pages/itemdetails/FilmPage.qml")
|
||||||
case "collection":
|
case "collection":
|
||||||
return Qt.resolvedUrl("pages/itemdetails/ColectionPage.qml")
|
return Qt.resolvedUrl("pages/itemdetails/CollectionPage.qml")
|
||||||
case "season":
|
case "season":
|
||||||
return Qt.resolvedUrl("pages/itemdetails/SeasonPage.qml")
|
return Qt.resolvedUrl("pages/itemdetails/SeasonPage.qml")
|
||||||
case "episode":
|
case "episode":
|
||||||
return Qt.resolvedUrl("pages/itemdetails/EpisodePage.qml")
|
return Qt.resolvedUrl("pages/itemdetails/EpisodePage.qml")
|
||||||
default:
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -202,7 +202,7 @@ Page {
|
||||||
progress: model.userData.PlayedPercentage / 100
|
progress: model.userData.PlayedPercentage / 100
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
pageStack.push(Utils.getPageUrl(model.type), {"itemId": model.id})
|
pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.id})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,17 +62,13 @@ BaseDetailPage {
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
clip: true
|
clip: true
|
||||||
}
|
}
|
||||||
Rectangle {
|
Shim {
|
||||||
anchors {
|
anchors {
|
||||||
left: parent.left
|
left: parent.left
|
||||||
bottom: parent.bottom
|
bottom: parent.bottom
|
||||||
right: parent.right
|
right: parent.right
|
||||||
}
|
}
|
||||||
height: itemName.height + Theme.paddingSmall * 2
|
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
|
visible: itemImage.status !== Image.Null
|
||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
|
@ -96,7 +92,7 @@ BaseDetailPage {
|
||||||
pageStack.push(Qt.resolvedUrl("CollectionPage.qml"), {"itemId": model.id})
|
pageStack.push(Qt.resolvedUrl("CollectionPage.qml"), {"itemId": model.id})
|
||||||
break;
|
break;
|
||||||
default:
|
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 {}
|
VerticalScrollDecorator {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The page for selecting a sort order
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: sortPageComponent
|
id: sortPageComponent
|
||||||
Page {
|
Page {
|
||||||
|
@ -139,8 +137,23 @@ BaseDetailPage {
|
||||||
}
|
}
|
||||||
text: model.name
|
text: model.name
|
||||||
}
|
}
|
||||||
onClicked: {
|
menu: ContextMenu {
|
||||||
collectionModel.sortBy = [model.value]
|
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()
|
collectionModel.reload()
|
||||||
pageStack.pop()
|
pageStack.pop()
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,7 +122,7 @@ BaseDetailPage {
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
elide: Text.ElideRight
|
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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@ BaseDetailPage {
|
||||||
delegate: LibraryItemDelegate {
|
delegate: LibraryItemDelegate {
|
||||||
poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags["Primary"], "Primary", {"maxHeight": height})
|
poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags["Primary"], "Primary", {"maxHeight": height})
|
||||||
title: model.name
|
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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
63
qml/pages/itemdetails/VideoPage.qml
Normal file
63
qml/pages/itemdetails/VideoPage.qml
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,6 +60,9 @@ void ApiModel::load(LoadType type) {
|
||||||
if (!m_sortBy.empty()) {
|
if (!m_sortBy.empty()) {
|
||||||
query.addQueryItem("SortBy", m_sortBy.join(","));
|
query.addQueryItem("SortBy", m_sortBy.join(","));
|
||||||
}
|
}
|
||||||
|
if (m_sortOrder != Unspecified) {
|
||||||
|
query.addQueryItem("SortOrder", m_sortOrder == Ascending ? "Ascending" : "Descending");
|
||||||
|
}
|
||||||
if (!m_imageTypes.empty()) {
|
if (!m_imageTypes.empty()) {
|
||||||
query.addQueryItem("ImageTypes", m_imageTypes.join(","));
|
query.addQueryItem("ImageTypes", m_imageTypes.join(","));
|
||||||
}
|
}
|
||||||
|
@ -208,7 +211,7 @@ void ApiModel::addQueryParameters(QUrlQuery &query) { Q_UNUSED(query)}
|
||||||
|
|
||||||
void registerModels(const char *URI) {
|
void registerModels(const char *URI) {
|
||||||
qmlRegisterUncreatableType<ApiModel>(URI, 1, 0, "ApiModel", "Is enum and base class");
|
qmlRegisterUncreatableType<ApiModel>(URI, 1, 0, "ApiModel", "Is enum and base class");
|
||||||
qmlRegisterUncreatableType<SortOrder>(URI, 1, 0, "SortOrder", "Is enum");
|
qmlRegisterUncreatableType<SortOptions>(URI, 1, 0, "SortOptions", "Is enum");
|
||||||
qmlRegisterType<PublicUserModel>(URI, 1, 0, "PublicUserModel");
|
qmlRegisterType<PublicUserModel>(URI, 1, 0, "PublicUserModel");
|
||||||
qmlRegisterType<UserViewModel>(URI, 1, 0, "UserViewModel");
|
qmlRegisterType<UserViewModel>(URI, 1, 0, "UserViewModel");
|
||||||
qmlRegisterType<UserItemModel>(URI, 1, 0, "UserItemModel");
|
qmlRegisterType<UserItemModel>(URI, 1, 0, "UserItemModel");
|
||||||
|
|
|
@ -31,9 +31,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
#include "jellyfinapiclient.h"
|
#include "jellyfinapiclient.h"
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
class SortOrder {
|
class SortOptions : public QObject{
|
||||||
Q_GADGET
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
|
explicit SortOptions (QObject *parent = nullptr) : QObject(parent) {}
|
||||||
enum SortBy {
|
enum SortBy {
|
||||||
Album,
|
Album,
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
|
@ -54,6 +55,7 @@ public:
|
||||||
Q_ENUM(SortBy)
|
Q_ENUM(SortBy)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Abstract model for displaying a REST JSON collection. Role names will be based on the fields encountered in the
|
* @brief Abstract model for displaying a REST JSON collection. Role names will be based on the fields encountered in the
|
||||||
* first record.
|
* first record.
|
||||||
|
@ -90,6 +92,13 @@ public:
|
||||||
};
|
};
|
||||||
Q_ENUM(ModelStatus)
|
Q_ENUM(ModelStatus)
|
||||||
|
|
||||||
|
enum SortOrder {
|
||||||
|
Unspecified,
|
||||||
|
Ascending,
|
||||||
|
Descending
|
||||||
|
};
|
||||||
|
Q_ENUM(SortOrder)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Creates a new basemodel
|
* @brief Creates a new basemodel
|
||||||
* @param path The path (relative to the baseUrl of JellyfinApiClient) to make the call to.
|
* @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(QString seasonId MEMBER m_seasonId NOTIFY seasonIdChanged)
|
||||||
Q_PROPERTY(QList<QString> imageTypes MEMBER m_imageTypes NOTIFY imageTypesChanged)
|
Q_PROPERTY(QList<QString> imageTypes MEMBER m_imageTypes NOTIFY imageTypesChanged)
|
||||||
Q_PROPERTY(bool recursive MEMBER m_recursive)
|
Q_PROPERTY(bool recursive MEMBER m_recursive)
|
||||||
|
Q_PROPERTY(SortOrder sortOrder MEMBER m_sortOrder NOTIFY sortOrderChanged)
|
||||||
|
|
||||||
// Path properties
|
// Path properties
|
||||||
Q_PROPERTY(QString show MEMBER m_show NOTIFY showChanged)
|
Q_PROPERTY(QString show MEMBER m_show NOTIFY showChanged)
|
||||||
|
@ -164,6 +174,7 @@ signals:
|
||||||
void limitChanged(int newLimit);
|
void limitChanged(int newLimit);
|
||||||
void parentIdChanged(QString newParentId);
|
void parentIdChanged(QString newParentId);
|
||||||
void sortByChanged(QList<QString> newSortOrder);
|
void sortByChanged(QList<QString> newSortOrder);
|
||||||
|
void sortOrderChanged(SortOrder newSortOrder);
|
||||||
void showChanged(QString newShow);
|
void showChanged(QString newShow);
|
||||||
void seasonIdChanged(QString newSeasonId);
|
void seasonIdChanged(QString newSeasonId);
|
||||||
void fieldsChanged(QList<QString> newFields);
|
void fieldsChanged(QList<QString> newFields);
|
||||||
|
@ -214,6 +225,7 @@ protected:
|
||||||
QList<QString> m_fields;
|
QList<QString> m_fields;
|
||||||
QList<QString> m_imageTypes;
|
QList<QString> m_imageTypes;
|
||||||
QList<QString> m_sortBy = {};
|
QList<QString> m_sortBy = {};
|
||||||
|
SortOrder m_sortOrder = Unspecified;
|
||||||
bool m_recursive = false;
|
bool m_recursive = false;
|
||||||
|
|
||||||
QHash<int, QByteArray> m_roles;
|
QHash<int, QByteArray> m_roles;
|
||||||
|
@ -228,7 +240,7 @@ private:
|
||||||
* @brief Generates roleNames based on the first record in m_array.
|
* @brief Generates roleNames based on the first record in m_array.
|
||||||
*/
|
*/
|
||||||
void generateFields();
|
void generateFields();
|
||||||
QString sortByToString(SortOrder::SortBy sortBy);
|
QString sortByToString(SortOptions::SortBy sortBy);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -93,6 +93,16 @@
|
||||||
<source>Date added</source>
|
<source>Date added</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Ascending</source>
|
||||||
|
<extracomment>Sort order</extracomment>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Descending</source>
|
||||||
|
<extracomment>Sort order</extracomment>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>CoverPage</name>
|
<name>CoverPage</name>
|
||||||
|
@ -104,11 +114,15 @@
|
||||||
<context>
|
<context>
|
||||||
<name>EpisodePage</name>
|
<name>EpisodePage</name>
|
||||||
<message>
|
<message>
|
||||||
<source>Episode %1–%2 Season %3</source>
|
<source>Episode %1–%2 | %3</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Episode %1 Season %2</source>
|
<source>Episode %1 | %2</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Overview</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
|
@ -118,6 +132,10 @@
|
||||||
<source>Released: %1 — Run time: %2</source>
|
<source>Released: %1 — Run time: %2</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Overview</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>LegalPage</name>
|
<name>LegalPage</name>
|
||||||
|
@ -301,6 +319,13 @@
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>VideoPage</name>
|
||||||
|
<message>
|
||||||
|
<source>Run time: %2</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>VideoTrackSelector</name>
|
<name>VideoTrackSelector</name>
|
||||||
<message>
|
<message>
|
||||||
|
|
Loading…
Reference in a new issue