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

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 src/serverdiscoverymodel.cpp
DISTFILES += \ DISTFILES += \
qml/Utils.js \
qml/components/GlassyBackground.qml \ qml/components/GlassyBackground.qml \
qml/components/LibraryItemDelegate.qml \ qml/components/LibraryItemDelegate.qml \
qml/components/MoreSection.qml \ qml/components/MoreSection.qml \
@ -35,8 +36,11 @@ DISTFILES += \
qml/components/VideoPlayer.qml \ qml/components/VideoPlayer.qml \
qml/components/itemdetails/EpisodeDetails.qml \ qml/components/itemdetails/EpisodeDetails.qml \
qml/components/itemdetails/FilmDetails.qml \ qml/components/itemdetails/FilmDetails.qml \
qml/components/itemdetails/PlayToolbar.qml \
qml/components/itemdetails/SeasonDetails.qml \ qml/components/itemdetails/SeasonDetails.qml \
qml/components/itemdetails/SeriesDetails.qml \ qml/components/itemdetails/SeriesDetails.qml \
qml/components/itemdetails/UnsupportedDetails.qml \
qml/components/itemdetails/VideoTrackSelector.qml \
qml/components/videoplayer/VideoHud.qml \ qml/components/videoplayer/VideoHud.qml \
qml/cover/CoverPage.qml \ qml/cover/CoverPage.qml \
qml/cover/PosterCover.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 { FastBlur {
cached: true
anchors.fill: backgroundImage anchors.fill: backgroundImage
source: backgroundImage source: backgroundImage
opacity: dimmedOpacity opacity: dimmedOpacity
radius: 100 radius: 50
} }
Image { Image {

View file

@ -18,6 +18,8 @@ SilicaItem {
readonly property bool landscape: videoOutput.contentRect.width > videoOutput.contentRect.height readonly property bool landscape: videoOutput.contentRect.width > videoOutput.contentRect.height
property MediaPlayer player property MediaPlayer player
readonly property bool hudVisible: !hud.hidden 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 // Force a Light on Dark theme since I doubt that there are persons who are willing to watch a Video
// on a white background. // 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 QtMultimedia 5.6
import Sailfish.Silica 1.0 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 * The video "hud" or controls. This is the overlay displayed over a video player, which contains controls
* and playback information. * and playback information.
@ -92,7 +94,7 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.horizontalPageMargin anchors.leftMargin: Theme.horizontalPageMargin
anchors.verticalCenter: progressSlider.verticalCenter anchors.verticalCenter: progressSlider.verticalCenter
text: timeToText(player.position) text: Utils.timeToText(player.position)
} }
Slider { Slider {
@ -112,21 +114,12 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Theme.horizontalPageMargin anchors.rightMargin: Theme.horizontalPageMargin
anchors.verticalCenter: progress.verticalCenter 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 { Connections {
target: player target: player

View file

@ -2,8 +2,13 @@ import QtQuick 2.6
import QtMultimedia 5.6 import QtMultimedia 5.6
import Sailfish.Silica 1.0 import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0
import "../components"
CoverBackground { CoverBackground {
readonly property MediaPlayer player: appWindow.mediaPlayer readonly property MediaPlayer player: appWindow.mediaPlayer
property var mData: appWindow.itemData
Rectangle { Rectangle {
anchors.fill: parent 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 { CoverActionList {
CoverAction { CoverAction {
id: playPause id: playPause
iconSource: player.playbackState === MediaPlayer.PlayingState ? "image://theme/icon-cover-pause" iconSource: player.playbackState === MediaPlayer.PlayingState ? "image://theme/icon-cover-pause"
: "image://theme/icon-cover-play" : "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 height: Theme.paddingLarge
} }
PlainLabel { Loader {
id: overviewText active: itemData != undefined
text: itemData.Overview asynchronous: true
visible: text.length > 0 width: parent.width
font.pixelSize: Theme.fontSizeSmall 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 {} HorizontalScrollDecorator {}
UserItemModel { UserItemLatestModel {
id: userItemModel id: userItemModel
apiClient: ApiClient apiClient: ApiClient
parentId: model.id parentId: model.id

View file

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

View file

@ -191,6 +191,12 @@ public:
explicit UserItemModel (QObject *parent = nullptr) explicit UserItemModel (QObject *parent = nullptr)
: ApiModel ("/Users/:user/Items", "Items", parent) {} : 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); void registerModels(const char *URI);

View file

@ -3,8 +3,7 @@
namespace Jellyfin { namespace Jellyfin {
MediaSource::MediaSource(QObject *parent) MediaSource::MediaSource(QObject *parent)
: QObject(parent), : QObject(parent) {
m_mediaPlayer(new QMediaPlayer(this)){
} }
@ -15,8 +14,8 @@ void MediaSource::fetchStreamUrl() {
params.addQueryItem("IsPlayback", "true"); params.addQueryItem("IsPlayback", "true");
params.addQueryItem("AutoOpenLiveStream", this->m_autoOpen ? "true" : "false"); params.addQueryItem("AutoOpenLiveStream", this->m_autoOpen ? "true" : "false");
params.addQueryItem("MediaSourceId", this->m_itemId); params.addQueryItem("MediaSourceId", this->m_itemId);
params.addQueryItem("SubtitleStreamIndex", "-1"); params.addQueryItem("SubtitleStreamIndex", QString::number(m_subtitleIndex));
params.addQueryItem("AudioStreamIndex", "0"); params.addQueryItem("AudioStreamIndex", QString::number(m_audioIndex));
QJsonObject root; QJsonObject root;
root["DeviceProfile"] = m_apiClient->playbackDeviceProfile(); root["DeviceProfile"] = m_apiClient->playbackDeviceProfile();
@ -36,11 +35,6 @@ void MediaSource::fetchStreamUrl() {
emit this->streamUrlChanged(this->m_streamUrl); emit this->streamUrlChanged(this->m_streamUrl);
qDebug() << "Found stream url: " << 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(); rep->deleteLater();
@ -52,10 +46,7 @@ void MediaSource::setItemId(const QString &newItemId) {
qWarning() << "apiClient is not set on this MediaSource instance! Aborting."; qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
return; return;
} }
if (m_mediaPlayer == nullptr) {
qWarning() << "mediaPlayer is not set on this MediaSource instance! Aborting.";
return;
}
this->m_itemId = newItemId; this->m_itemId = newItemId;
// Deinitialize the streamUrl // Deinitialize the streamUrl
setStreamUrl(""); setStreamUrl("");
@ -70,15 +61,15 @@ void MediaSource::setStreamUrl(const QString &streamUrl) {
} }
void MediaSource::play() { void MediaSource::play() {
this->m_mediaPlayer->play(); //todo: playback reporting
} }
void MediaSource::pause() { void MediaSource::pause() {
this->m_mediaPlayer->pause(); //todo: playback reporting
} }
void MediaSource::stop() { 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 itemId READ itemId WRITE setItemId NOTIFY itemIdChanged)
Q_PROPERTY(QString streamUrl READ streamUrl NOTIFY streamUrlChanged) Q_PROPERTY(QString streamUrl READ streamUrl NOTIFY streamUrlChanged)
Q_PROPERTY(bool autoOpen MEMBER m_autoOpen NOTIFY autoOpenChanged) Q_PROPERTY(bool autoOpen MEMBER m_autoOpen NOTIFY autoOpenChanged)
Q_PROPERTY(QMediaPlayer *mediaPlayer READ mediaPlayer) Q_PROPERTY(int audioIndex MEMBER m_audioIndex NOTIFY audioIndexChanged)
Q_PROPERTY(bool autoPlay MEMBER m_autoPlay) Q_PROPERTY(int subtitleIndex MEMBER m_subtitleIndex NOTIFY subtitleIndexChanged)
QString itemId() const { return m_itemId; } QString itemId() const { return m_itemId; }
void setItemId(const QString &newItemId); void setItemId(const QString &newItemId);
QString streamUrl() const { return m_streamUrl; } QString streamUrl() const { return m_streamUrl; }
QMediaPlayer *mediaPlayer() { return m_mediaPlayer; }
signals: signals:
void itemIdChanged(const QString &newItemId); void itemIdChanged(const QString &newItemId);
void streamUrlChanged(const QString &newStreamUrl); void streamUrlChanged(const QString &newStreamUrl);
void autoOpenChanged(bool autoOpen); void autoOpenChanged(bool autoOpen);
void audioIndexChanged(int audioIndex);
void subtitleIndexChanged(int subtitleIndex);
public slots: public slots:
void play(); void play();
@ -43,15 +43,16 @@ public slots:
private: private:
ApiClient *m_apiClient = nullptr; ApiClient *m_apiClient = nullptr;
QMediaPlayer *m_mediaPlayer = nullptr;
QString m_itemId; QString m_itemId;
QString m_streamUrl; QString m_streamUrl;
QString m_playSessionId; QString m_playSessionId;
int m_audioIndex = 0;
int m_subtitleIndex = -1;
/** /**
* @brief Whether to automatically open the livestream of the item; * @brief Whether to automatically open the livestream of the item;
*/ */
bool m_autoOpen = false; bool m_autoOpen = false;
bool m_autoPlay = false;
void fetchStreamUrl(); void fetchStreamUrl();
void setStreamUrl(const QString &streamUrl); void setStreamUrl(const QString &streamUrl);

View file

@ -50,6 +50,13 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context>
<name>FilmDetails</name>
<message>
<source>Released: %1 Run time: %2</source>
<translation type="unfinished"></translation>
</message>
</context>
<context> <context>
<name>LegalPage</name> <name>LegalPage</name>
<message> <message>
@ -57,11 +64,11 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<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> <translation type="unfinished"></translation>
</message> </message>
<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> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
@ -118,6 +125,17 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </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> <context>
<name>UserGridDelegate</name> <name>UserGridDelegate</name>
<message> <message>
@ -125,4 +143,20 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </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> </TS>