Added track selection and minor UI improvements

This commit is contained in:
Chris Josten 2020-09-25 17:14:44 +02:00
parent 1eb6a8fb5d
commit 020c968f9c
18 changed files with 271 additions and 68 deletions

View File

@ -26,6 +26,7 @@ SOURCES += \
src/serverdiscoverymodel.cpp
DISTFILES += \
qml/Utils.js \
qml/components/GlassyBackground.qml \
qml/components/LibraryItemDelegate.qml \
qml/components/MoreSection.qml \
@ -35,8 +36,11 @@ DISTFILES += \
qml/components/VideoPlayer.qml \
qml/components/itemdetails/EpisodeDetails.qml \
qml/components/itemdetails/FilmDetails.qml \
qml/components/itemdetails/PlayToolbar.qml \
qml/components/itemdetails/SeasonDetails.qml \
qml/components/itemdetails/SeriesDetails.qml \
qml/components/itemdetails/UnsupportedDetails.qml \
qml/components/itemdetails/VideoTrackSelector.qml \
qml/components/videoplayer/VideoHud.qml \
qml/cover/CoverPage.qml \
qml/cover/PosterCover.qml \

19
qml/Utils.js Normal file
View File

@ -0,0 +1,19 @@
.pragma library
/**
* Converts miliseconds to a h:mm:ss format
*/
function timeToText(time) {
if (time < 0) return "??:??:??"
var hours = Math.floor(time / (60 * 60 * 1000))
var left = time % (60 * 60 * 1000)
var minutes = Math.floor(left / (60 * 1000))
left = time % (60 * 1000)
var seconds = Math.floor(left / 1000)
return hours + ":" + (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "")+ seconds
}
function ticksToText(ticks) {
return timeToText(ticks / 10000);
}

View File

@ -26,10 +26,11 @@ Rectangle {
}
FastBlur {
cached: true
anchors.fill: backgroundImage
source: backgroundImage
opacity: dimmedOpacity
radius: 100
radius: 50
}
Image {

View File

@ -18,6 +18,8 @@ SilicaItem {
readonly property bool landscape: videoOutput.contentRect.width > videoOutput.contentRect.height
property MediaPlayer player
readonly property bool hudVisible: !hud.hidden
property alias audioTrack: mediaSource.audioIndex
property alias subtitleTrack: mediaSource.subtitleIndex
// Force a Light on Dark theme since I doubt that there are persons who are willing to watch a Video
// on a white background.

View File

@ -1,5 +1,41 @@
import QtQuick 2.0
import QtQuick 2.6
import Sailfish.Silica 1.0
import "../"
import "../../Utils.js" as Utils
Column {
property var itemData
spacing: Theme.paddingMedium
PlayToolbar {
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
}
PlainLabel {
text: "sub: %1 dub: %2".arg(trackSelector.subtitleTrack).arg(trackSelector.audioTrack)
}
PlainLabel {
id: tinyDetails
text: qsTr("Released: %1 — Run time: %2").arg(itemData.ProductionYear).arg(Utils.ticksToText(itemData.RunTimeTicks))
}
PlainLabel {
id: overviewText
text: itemData.Overview
font.pixelSize: Theme.fontSizeSmall
color: Theme.secondaryHighlightColor
}
Item {
}

View File

@ -0,0 +1,23 @@
import QtQuick 2.6
import Sailfish.Silica 1.0
Row {
signal playPressed()
anchors {
//left: parent.left
right: parent.right
leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin
}
spacing: Theme.paddingMedium
IconButton {
id: favouriteButton
icon.source: "image://theme/icon-m-favorite"
}
IconButton {
id: playButton
icon.source: "image://theme/icon-l-play"
onPressed: playPressed()
}
}

View File

@ -0,0 +1,8 @@
import QtQuick 2.6
import Sailfish.Silica 1.0
ViewPlaceholder {
enabled: true
text: qsTr("Item type unsupported")
hintText: qsTr("This is still an alpha version :)")
}

View File

@ -0,0 +1,67 @@
import QtQuick 2.6
import Sailfish.Silica 1.0
Column {
property var tracks
readonly property int audioTrack: audioSelector.currentItem ? audioSelector.currentItem._index : 0
readonly property int subtitleTrack: subitleSelector.currentItem._index
ListModel {
id: audioModel
}
ListModel {
id: subtitleModel
}
ComboBox {
id: audioSelector
label: qsTr("Audio track")
menu: ContextMenu {
Repeater {
model: audioModel
MenuItem {
readonly property int _index: model.Index
text: model.DisplayTitle
}
}
}
}
ComboBox {
id: subitleSelector
label: qsTr("Subtitle track")
menu: ContextMenu {
MenuItem {
readonly property int _index: -1
//: Value in ComboBox to disable subtitles
text: qsTr("Off")
}
Repeater {
model: subtitleModel
MenuItem {
readonly property int _index: model.Index
text: model.DisplayTitle
}
}
}
}
onTracksChanged: {
audioModel.clear()
subtitleModel.clear()
for(var i = 0; i < tracks.length; i++) {
var track = tracks[i];
switch(track.Type) {
case "Audio":
audioModel.append(track)
break;
case "Subtitle":
subtitleModel.append(track)
break;
default:
break;
}
}
}
}

View File

@ -2,6 +2,8 @@ import QtQuick 2.6
import QtMultimedia 5.6
import Sailfish.Silica 1.0
import "../../Utils.js" as Utils
/**
* The video "hud" or controls. This is the overlay displayed over a video player, which contains controls
* and playback information.
@ -92,7 +94,7 @@ Item {
anchors.left: parent.left
anchors.leftMargin: Theme.horizontalPageMargin
anchors.verticalCenter: progressSlider.verticalCenter
text: timeToText(player.position)
text: Utils.timeToText(player.position)
}
Slider {
@ -112,21 +114,12 @@ Item {
anchors.right: parent.right
anchors.rightMargin: Theme.horizontalPageMargin
anchors.verticalCenter: progress.verticalCenter
text: timeToText(player.duration)
text: Utils.timeToText(player.duration)
}
}
}
function timeToText(time) {
if (time < 0) return "??:??:??"
var hours = Math.floor(time / (60 * 60 * 1000))
var left = time % (60 * 60 * 1000)
var minutes = Math.floor(left / (60 * 1000))
left = time % (60 * 1000)
var seconds = Math.floor(left / 1000)
return hours + ":" + (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "")+ seconds
}
Connections {
target: player

View File

@ -2,8 +2,13 @@ import QtQuick 2.6
import QtMultimedia 5.6
import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0
import "../components"
CoverBackground {
readonly property MediaPlayer player: appWindow.mediaPlayer
property var mData: appWindow.itemData
Rectangle {
anchors.fill: parent
@ -17,12 +22,27 @@ CoverBackground {
}*/
}
// As a temporary fallback, use the poster image
RemoteImage {
anchors.fill: parent
source: mData.ImageTags["Primary"] ? ApiClient.baseUrl + "/Items/" + mData.Id
+ "/Images/Primary?maxHeight=" + height + "&tag=" + mData.ImageTags["Primary"]
: ""
fillMode: Image.PreserveAspectCrop
}
CoverActionList {
CoverAction {
id: playPause
iconSource: player.playbackState === MediaPlayer.PlayingState ? "image://theme/icon-cover-pause"
: "image://theme/icon-cover-play"
onTriggered: {
if (player.playbackState === MediaPlayer.PlayingState) {
player.pause()
} else {
player.play()
}
}
}
}

View File

@ -81,37 +81,24 @@ Page {
height: Theme.paddingLarge
}
PlainLabel {
id: overviewText
text: itemData.Overview
visible: text.length > 0
font.pixelSize: Theme.fontSizeSmall
Loader {
active: itemData != undefined
asynchronous: true
width: parent.width
source: {
switch (itemData.Type){
case "Movie":
return Qt.resolvedUrl("../components/itemdetails/FilmDetails.qml")
default:
return Qt.resolvedUrl("../components/itemdetails/UnsupportedDetails.qml")
}
}
onLoaded: {
item.itemData = Qt.binding(function() { return pageRoot.itemData; })
}
}
Item {
visible: overviewText.visible
width: 1
height: Theme.paddingLarge
}
Row {
anchors {
//left: parent.left
right: parent.right
leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin
}
spacing: Theme.paddingMedium
IconButton {
id: favouriteButton
icon.source: "image://theme/icon-m-favorite"
}
IconButton {
id: playButton
icon.source: "image://theme/icon-l-play"
onPressed: pageStack.push(Qt.resolvedUrl("VideoPage.qml"), {"itemId": itemId, "itemData": itemData})
}
}
}
}

View File

@ -84,7 +84,7 @@ Page {
}
}
HorizontalScrollDecorator {}
UserItemModel {
UserItemLatestModel {
id: userItemModel
apiClient: ApiClient
parentId: model.id

View File

@ -13,6 +13,9 @@ Page {
id: videoPage
property string itemId
property var itemData
property int audioTrack
property int subtitleTrack
allowedOrientations: Orientation.All
showNavigationIndicator: videoPlayer.hudVisible
@ -22,6 +25,8 @@ Page {
itemId: videoPage.itemId
player: appWindow.mediaPlayer
title: itemData.Name
audioTrack: videoPage.audioTrack
subtitleTrack: videoPage.subtitleTrack
onLandscapeChanged: {
console.log("Is landscape: " + landscape)
@ -31,8 +36,13 @@ Page {
}
onStatusChanged: {
if (status == PageStatus.Inactive) {
switch(status) {
case PageStatus.Inactive:
videoPlayer.stop()
break;
case PageStatus.Active:
appWindow.itemData = videoPage.itemData
break;
}
}
}

View File

@ -111,5 +111,6 @@ void registerModels(const char *URI) {
qmlRegisterType<PublicUserModel>(URI, 1, 0, "PublicUserModel");
qmlRegisterType<UserViewModel>(URI, 1, 0, "UserViewModel");
qmlRegisterType<UserItemModel>(URI, 1, 0, "UserItemModel");
qmlRegisterType<UserItemLatestModel>(URI, 1, 0, "UserItemLatestModel");
}
}

View File

@ -191,6 +191,12 @@ public:
explicit UserItemModel (QObject *parent = nullptr)
: ApiModel ("/Users/:user/Items", "Items", parent) {}
};
class UserItemLatestModel : public ApiModel {
public:
explicit UserItemLatestModel (QObject *parent = nullptr)
: ApiModel ("/Users/:user/Items/Latest", "", parent) {}
};
void registerModels(const char *URI);

View File

@ -3,8 +3,7 @@
namespace Jellyfin {
MediaSource::MediaSource(QObject *parent)
: QObject(parent),
m_mediaPlayer(new QMediaPlayer(this)){
: QObject(parent) {
}
@ -15,8 +14,8 @@ void MediaSource::fetchStreamUrl() {
params.addQueryItem("IsPlayback", "true");
params.addQueryItem("AutoOpenLiveStream", this->m_autoOpen ? "true" : "false");
params.addQueryItem("MediaSourceId", this->m_itemId);
params.addQueryItem("SubtitleStreamIndex", "-1");
params.addQueryItem("AudioStreamIndex", "0");
params.addQueryItem("SubtitleStreamIndex", QString::number(m_subtitleIndex));
params.addQueryItem("AudioStreamIndex", QString::number(m_audioIndex));
QJsonObject root;
root["DeviceProfile"] = m_apiClient->playbackDeviceProfile();
@ -36,11 +35,6 @@ void MediaSource::fetchStreamUrl() {
emit this->streamUrlChanged(this->m_streamUrl);
qDebug() << "Found stream url: " << this->m_streamUrl;
/*QNetworkRequest req;
req.setUrl(this->m_streamUrl);
m_apiClient->addTokenHeader(req);
m_mediaPlayer->setMedia(QMediaContent(req));
if (m_autoPlay) m_mediaPlayer->play();*/
}
rep->deleteLater();
@ -52,10 +46,7 @@ void MediaSource::setItemId(const QString &newItemId) {
qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
return;
}
if (m_mediaPlayer == nullptr) {
qWarning() << "mediaPlayer is not set on this MediaSource instance! Aborting.";
return;
}
this->m_itemId = newItemId;
// Deinitialize the streamUrl
setStreamUrl("");
@ -70,15 +61,15 @@ void MediaSource::setStreamUrl(const QString &streamUrl) {
}
void MediaSource::play() {
this->m_mediaPlayer->play();
//todo: playback reporting
}
void MediaSource::pause() {
this->m_mediaPlayer->pause();
//todo: playback reporting
}
void MediaSource::stop() {
this->m_mediaPlayer->stop();
//todo: playback reporting
}
}

View File

@ -22,19 +22,19 @@ public:
Q_PROPERTY(QString itemId READ itemId WRITE setItemId NOTIFY itemIdChanged)
Q_PROPERTY(QString streamUrl READ streamUrl NOTIFY streamUrlChanged)
Q_PROPERTY(bool autoOpen MEMBER m_autoOpen NOTIFY autoOpenChanged)
Q_PROPERTY(QMediaPlayer *mediaPlayer READ mediaPlayer)
Q_PROPERTY(bool autoPlay MEMBER m_autoPlay)
Q_PROPERTY(int audioIndex MEMBER m_audioIndex NOTIFY audioIndexChanged)
Q_PROPERTY(int subtitleIndex MEMBER m_subtitleIndex NOTIFY subtitleIndexChanged)
QString itemId() const { return m_itemId; }
void setItemId(const QString &newItemId);
QString streamUrl() const { return m_streamUrl; }
QMediaPlayer *mediaPlayer() { return m_mediaPlayer; }
signals:
void itemIdChanged(const QString &newItemId);
void streamUrlChanged(const QString &newStreamUrl);
void autoOpenChanged(bool autoOpen);
void audioIndexChanged(int audioIndex);
void subtitleIndexChanged(int subtitleIndex);
public slots:
void play();
@ -43,15 +43,16 @@ public slots:
private:
ApiClient *m_apiClient = nullptr;
QMediaPlayer *m_mediaPlayer = nullptr;
QString m_itemId;
QString m_streamUrl;
QString m_playSessionId;
int m_audioIndex = 0;
int m_subtitleIndex = -1;
/**
* @brief Whether to automatically open the livestream of the item;
*/
bool m_autoOpen = false;
bool m_autoPlay = false;
void fetchStreamUrl();
void setStreamUrl(const QString &streamUrl);

View File

@ -50,6 +50,13 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>FilmDetails</name>
<message>
<source>Released: %1 Run time: %2</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LegalPage</name>
<message>
@ -57,11 +64,11 @@
<translation type="unfinished"></translation>
</message>
<message>
<source>The Sailfin application contains some code from other projects. Without them, Sailfin would not be possible!</source>
<source>This program contains small snippets of code taken from &lt;a href=&quot;%1&quot;&gt;%2&lt;/a&gt;, which is licensed under the %3 license:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>This program contains small snippets of code taken from &lt;a href=&quot;%1&quot;&gt;%2&lt;/a&gt;, which is licensed under the %3 license:</source>
<source>Sailfin contains code taken from other projects. Without them, Sailfin would not be possible!</source>
<translation type="unfinished"></translation>
</message>
</context>
@ -118,6 +125,17 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>UnsupportedDetails</name>
<message>
<source>Item type unsupported</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>This is still an alpha version :)</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>UserGridDelegate</name>
<message>
@ -125,4 +143,20 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>VideoTrackSelector</name>
<message>
<source>Audio track</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Subtitle track</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Off</source>
<extracomment>Value in ComboBox to disable subtitles</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
</TS>