diff --git a/harbour-sailfin.pro b/harbour-sailfin.pro index eb44f57..e635689 100644 --- a/harbour-sailfin.pro +++ b/harbour-sailfin.pro @@ -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 \ diff --git a/qml/Utils.js b/qml/Utils.js new file mode 100644 index 0000000..39dd838 --- /dev/null +++ b/qml/Utils.js @@ -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); +} diff --git a/qml/components/GlassyBackground.qml b/qml/components/GlassyBackground.qml index cb2fcf8..6c94fc0 100644 --- a/qml/components/GlassyBackground.qml +++ b/qml/components/GlassyBackground.qml @@ -26,10 +26,11 @@ Rectangle { } FastBlur { + cached: true anchors.fill: backgroundImage source: backgroundImage opacity: dimmedOpacity - radius: 100 + radius: 50 } Image { diff --git a/qml/components/VideoPlayer.qml b/qml/components/VideoPlayer.qml index 5ccd748..73d8f30 100644 --- a/qml/components/VideoPlayer.qml +++ b/qml/components/VideoPlayer.qml @@ -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. diff --git a/qml/components/itemdetails/FilmDetails.qml b/qml/components/itemdetails/FilmDetails.qml index 9c36e13..d80a7f4 100644 --- a/qml/components/itemdetails/FilmDetails.qml +++ b/qml/components/itemdetails/FilmDetails.qml @@ -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 { } diff --git a/qml/components/itemdetails/PlayToolbar.qml b/qml/components/itemdetails/PlayToolbar.qml new file mode 100644 index 0000000..d13b3f9 --- /dev/null +++ b/qml/components/itemdetails/PlayToolbar.qml @@ -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() + } +} diff --git a/qml/components/itemdetails/UnsupportedDetails.qml b/qml/components/itemdetails/UnsupportedDetails.qml new file mode 100644 index 0000000..3754d3f --- /dev/null +++ b/qml/components/itemdetails/UnsupportedDetails.qml @@ -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 :)") +} diff --git a/qml/components/itemdetails/VideoTrackSelector.qml b/qml/components/itemdetails/VideoTrackSelector.qml new file mode 100644 index 0000000..a0f8edb --- /dev/null +++ b/qml/components/itemdetails/VideoTrackSelector.qml @@ -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; + } + } + } +} diff --git a/qml/components/videoplayer/VideoHud.qml b/qml/components/videoplayer/VideoHud.qml index cf77b6c..ca6e5ba 100644 --- a/qml/components/videoplayer/VideoHud.qml +++ b/qml/components/videoplayer/VideoHud.qml @@ -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 diff --git a/qml/cover/VideoCover.qml b/qml/cover/VideoCover.qml index c34bf2b..ac4a079 100644 --- a/qml/cover/VideoCover.qml +++ b/qml/cover/VideoCover.qml @@ -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() + } + } } } diff --git a/qml/pages/DetailPage.qml b/qml/pages/DetailPage.qml index c2c866f..f649e8b 100644 --- a/qml/pages/DetailPage.qml +++ b/qml/pages/DetailPage.qml @@ -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}) - } - } } } diff --git a/qml/pages/MainPage.qml b/qml/pages/MainPage.qml index f28f846..c04a7f8 100644 --- a/qml/pages/MainPage.qml +++ b/qml/pages/MainPage.qml @@ -84,7 +84,7 @@ Page { } } HorizontalScrollDecorator {} - UserItemModel { + UserItemLatestModel { id: userItemModel apiClient: ApiClient parentId: model.id diff --git a/qml/pages/VideoPage.qml b/qml/pages/VideoPage.qml index d67b37d..467d5f1 100644 --- a/qml/pages/VideoPage.qml +++ b/qml/pages/VideoPage.qml @@ -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; } } } diff --git a/src/jellyfinapimodel.cpp b/src/jellyfinapimodel.cpp index 49c78e6..183b9a2 100644 --- a/src/jellyfinapimodel.cpp +++ b/src/jellyfinapimodel.cpp @@ -111,5 +111,6 @@ void registerModels(const char *URI) { qmlRegisterType(URI, 1, 0, "PublicUserModel"); qmlRegisterType(URI, 1, 0, "UserViewModel"); qmlRegisterType(URI, 1, 0, "UserItemModel"); + qmlRegisterType(URI, 1, 0, "UserItemLatestModel"); } } diff --git a/src/jellyfinapimodel.h b/src/jellyfinapimodel.h index 07fb979..1ec0f5e 100644 --- a/src/jellyfinapimodel.h +++ b/src/jellyfinapimodel.h @@ -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); diff --git a/src/jellyfinmediasource.cpp b/src/jellyfinmediasource.cpp index 8e320ec..a8a0155 100644 --- a/src/jellyfinmediasource.cpp +++ b/src/jellyfinmediasource.cpp @@ -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 } } diff --git a/src/jellyfinmediasource.h b/src/jellyfinmediasource.h index e19a46c..566f9cd 100644 --- a/src/jellyfinmediasource.h +++ b/src/jellyfinmediasource.h @@ -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); diff --git a/translations/harbour-sailfin.ts b/translations/harbour-sailfin.ts index beedf38..08fcd9f 100644 --- a/translations/harbour-sailfin.ts +++ b/translations/harbour-sailfin.ts @@ -50,6 +50,13 @@ + + FilmDetails + + Released: %1 — Run time: %2 + + + LegalPage @@ -57,11 +64,11 @@ - The Sailfin application contains some code from other projects. Without them, Sailfin would not be possible! + This program contains small snippets of code taken from <a href="%1">%2</a>, which is licensed under the %3 license: - This program contains small snippets of code taken from <a href="%1">%2</a>, which is licensed under the %3 license: + Sailfin contains code taken from other projects. Without them, Sailfin would not be possible! @@ -118,6 +125,17 @@ + + UnsupportedDetails + + Item type unsupported + + + + This is still an alpha version :) + + + UserGridDelegate @@ -125,4 +143,20 @@ + + VideoTrackSelector + + Audio track + + + + Subtitle track + + + + Off + Value in ComboBox to disable subtitles + + +