diff --git a/sailfish/qml/Utils.js b/sailfish/qml/Utils.js index f31312d..01c1efe 100644 --- a/sailfish/qml/Utils.js +++ b/sailfish/qml/Utils.js @@ -74,13 +74,14 @@ function usePortraitCover(itemType) { /** * Returns the page url for a certain item type. */ -function getPageUrl(mediaType, itemType) { +function getPageUrl(mediaType, itemType, isFolder) { switch (itemType.toLowerCase()) { case "series": return Qt.resolvedUrl("pages/itemdetails/SeriesPage.qml") case "movie": return Qt.resolvedUrl("pages/itemdetails/FilmPage.qml") case "collection": + case "photoalbum": return Qt.resolvedUrl("pages/itemdetails/CollectionPage.qml") case "season": return Qt.resolvedUrl("pages/itemdetails/SeasonPage.qml") @@ -88,8 +89,10 @@ function getPageUrl(mediaType, itemType) { return Qt.resolvedUrl("pages/itemdetails/EpisodePage.qml") case "musicalbum": return Qt.resolvedUrl("pages/itemdetails/MusicAlbumPage.qml") + case "photo": + return Qt.resolvedUrl("pages/itemdetails/PhotoPage.qml") default: - switch (mediaType ? mediaType.toLowerCase() : "folder") { + switch (mediaType ? mediaType.toLowerCase() : isFolder ? "folder" : "") { case "folder": return Qt.resolvedUrl("pages/itemdetails/CollectionPage.qml") case "video": @@ -97,6 +100,7 @@ function getPageUrl(mediaType, itemType) { case "photo": return Qt.resolvedUrl("pages/itemdetails/PhotoPage.qml") default: + if (isFolder) return Qt.resolvedUrl("pages/itemdetails/CollectionPage.qml") return Qt.resolvedUrl("pages/itemdetails/UnsupportedPage.qml") } } diff --git a/sailfish/qml/components/PlayToolbar.qml b/sailfish/qml/components/PlayToolbar.qml index faf1b6b..0b6fac1 100644 --- a/sailfish/qml/components/PlayToolbar.qml +++ b/sailfish/qml/components/PlayToolbar.qml @@ -25,13 +25,14 @@ Column { property real imageAspectRatio: 1.0 property real playProgress: 0.0 property bool favourited: false + property alias imageBlurhash: playImage.blurhash signal playPressed(bool startFromBeginning) spacing: Theme.paddingLarge BackgroundItem { width: parent.width height: width / imageAspectRatio - HighlightImage { + RemoteImage { id: playImage anchors.fill: parent fillMode: Image.PreserveAspectCrop diff --git a/sailfish/qml/components/RemoteImage.qml b/sailfish/qml/components/RemoteImage.qml index 18af76d..88e8988 100644 --- a/sailfish/qml/components/RemoteImage.qml +++ b/sailfish/qml/components/RemoteImage.qml @@ -23,7 +23,10 @@ import Sailfish.Silica 1.0 import nl.netsoj.chris.blurhash 1.0 /** - * An image for "remote" images (loaded over e.g. http), with a spinner and a fallback image + * An image for "remote" images (loaded over e.g. http), with a spinner and a fallback image. + * + * If placed in a page, it will delay the loading of the image until the transition is over, + * displaying a simpler blurhash instead, to reduce the stutter during the transition */ SilicaItem { id: root diff --git a/sailfish/qml/harbour-sailfin.qml.autosave b/sailfish/qml/harbour-sailfin.qml.autosave new file mode 100644 index 0000000..15648fe --- /dev/null +++ b/sailfish/qml/harbour-sailfin.qml.autosave @@ -0,0 +1,113 @@ +/* +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.0 +import Sailfish.Silica 1.0 +import QtMultimedia 5.6 +import nl.netsoj.chris.Jellyfin 1.0 + +import Nemo.Notifications 1.0 +import Nemo.KeepAlive 1.2 + +import "components" +import "pages" + +ApplicationWindow { + id: appWindow + property bool _hasInitialized: false + // The global mediaPlayer instance + readonly property MediaPlayer mediaPlayer: _mediaPlayer + + // Data of the currently selected item. For use on the cover. + property var itemData + // Id of the collection currently browsing. For use on the cover. + property string collectionId + + //FIXME: proper error handling + Connections { + target: ApiClient + onNetworkError: errorNotification.show("Network error: " + error) + onConnectionFailed: errorNotification.show("Connect error: " + error) + //onConnectionSuccess: errorNotification.show("Success: " + loginMessage) + onSetupRequired: { + var isInSetup = pageStack.find(function (page) { return typeof page._isSetupPage !== "undefined" }) !== null + console.log("Is in setup: " + isInSetup) + if (!isInSetup) { + pageStack.replace(Qt.resolvedUrl("pages/setup/AddServerPage.qml"), {"backNavigation": false}); + } + } + } + + initialPage: Component { + MainPage { + Connections { + target: ApiClient + // Replace the MainPage if no server was set up. + + } + onStatusChanged: { + if (status == PageStatus.Active && !_hasInitialized) { + _hasInitialized = true; + ApiClient.restoreSavedSession(); + } + } + } + } + cover: { + if ([MediaPlayer.NoMedia, MediaPlayer.InvalidMedia, MediaPlayer.UnknownStatus].indexOf(mediaPlayer.status) >= 0) { + if (itemData) { + return Qt.resolvedUrl("cover/PosterCover.qml") + } else { + return Qt.resolvedUrl("cover/CoverPage.qml") + } + } else if (mediaPlayer.hasVideo){ + return Qt.resolvedUrl("cover/VideoCover.qml") + } + } + allowedOrientations: Orientation.All + + Notification { + id: errorNotification + previewSummary: "foo" + isTransient: true + + function show(data) { + previewSummary = data; + publish(); + } + } + + MediaPlayer { + id: _mediaPlayer + autoPlay: true + } + + // Keep the sytem alive while playing media + KeepAlive { + enabled: _mediaPlayer.playbackState == MediaPlayer.PlayingState + } + + DisplayBlanking { + preventBlanking: _mediaPlayer.playbackState == MediaPlayer.PlayingState && _mediaPlayer.hasVideo + } + + DockedPanel { + } + +} diff --git a/sailfish/qml/pages/MainPage.qml b/sailfish/qml/pages/MainPage.qml index aedd264..4877256 100644 --- a/sailfish/qml/pages/MainPage.qml +++ b/sailfish/qml/pages/MainPage.qml @@ -226,7 +226,7 @@ Page { progress: (typeof model.userData !== "undefined") ? model.userData.playedPercentage / 100 : 0.0 onClicked: { - pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.id}) + pageStack.push(Utils.getPageUrl(model.mediaType, model.type, model.isFolder), {"itemId": model.id}) } } } diff --git a/sailfish/qml/pages/itemdetails/BaseDetailPage.qml b/sailfish/qml/pages/itemdetails/BaseDetailPage.qml index 5fca4c3..efc0e99 100644 --- a/sailfish/qml/pages/itemdetails/BaseDetailPage.qml +++ b/sailfish/qml/pages/itemdetails/BaseDetailPage.qml @@ -39,22 +39,18 @@ Page { //property var itemData: ({}) property bool _loading: jItem.status === "Loading" readonly property bool hasLogo: (typeof itemData.ImageTags !== "undefined") && (typeof itemData.ImageTags["Logo"] !== "undefined") - readonly property var _backdropImages: itemData.backdropImageTags - readonly property var _parentBackdropImages: itemData.parentBackdropImageTags property string _chosenBackdropImage: "" readonly property string parentId: itemData.ParentId || "" - on_BackdropImagesChanged: updateBackdrop() - on_ParentBackdropImagesChanged: updateBackdrop() - function updateBackdrop() { + var rand = 0; if (itemData.backdropImageTags.length > 0) { - var rand = Math.floor(Math.random() * (_backdropImages.length - 0.001)) + rand = Math.floor(Math.random() * (itemData.backdropImageTags.length - 0.001)) console.log("Random: ", rand) - _chosenBackdropImage = ApiClient.baseUrl + "/Items/" + itemId + "/Images/Backdrop/" + rand + "?tag=" + _backdropImages[rand] + "&maxHeight" + height - //_chosenBackdropImage = Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Backdrop/" + rand) + _chosenBackdropImage = ApiClient.baseUrl + "/Items/" + itemId + "/Images/Backdrop/" + rand + "?tag=" +itemData.backdropImageTags[rand] + "&maxHeight" + height } else if (itemData.parentBackdropImageTags.length > 0) { - _chosenBackdropImage = ApiClient.baseUrl + "/Items/" + itemData.parentBackdropItemId + "/Images/Backdrop/0?tag=" + _parentBackdropImages[0] + rand = Math.floor(Math.random() * (itemData.parentBackdropImageTags.length - 0.001)) + _chosenBackdropImage = ApiClient.baseUrl + "/Items/" + itemData.parentBackdropItemId + "/Images/Backdrop/" + rand + "?tag=" + itemData.parentBackdropImageTags[0] } } @@ -76,11 +72,6 @@ Page { visible: false } - Text { - color: "red" - text: _chosenBackdropImage || "No backdrop" - } - PageBusyIndicator { running: pageRoot._loading } @@ -116,6 +107,9 @@ Page { onStatusChanged: { console.log("Status changed: " + newStatus, JSON.stringify(jItem)) console.log(jItem.mediaStreams) + if (status == JellyfinItem.Ready) { + updateBackdrop() + } } } diff --git a/sailfish/qml/pages/itemdetails/CollectionPage.qml b/sailfish/qml/pages/itemdetails/CollectionPage.qml index 9801d93..05bc79a 100644 --- a/sailfish/qml/pages/itemdetails/CollectionPage.qml +++ b/sailfish/qml/pages/itemdetails/CollectionPage.qml @@ -40,7 +40,7 @@ BaseDetailPage { anchors.fill: parent model: collectionModel cellWidth: Constants.libraryDelegateWidth - cellHeight: Utils.usePortraitCover(itemData.collectionType) ? Constants.libraryDelegatePosterHeight + cellHeight: Utils.usePortraitCover(itemData.type) ? Constants.libraryDelegatePosterHeight : Constants.libraryDelegateHeight visible: itemData.status !== JellyfinItem.Error @@ -90,7 +90,7 @@ BaseDetailPage { horizontalAlignment: Text.AlignLeft font.pixelSize: Theme.fontSizeSmall } - onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.id}) + onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type, model.isFolder), {"itemId": model.id}) } ViewPlaceholder { diff --git a/sailfish/qml/pages/itemdetails/SeriesPage.qml b/sailfish/qml/pages/itemdetails/SeriesPage.qml index 1cab136..9a7b9b7 100644 --- a/sailfish/qml/pages/itemdetails/SeriesPage.qml +++ b/sailfish/qml/pages/itemdetails/SeriesPage.qml @@ -78,13 +78,14 @@ BaseDetailPage { model: showSeasonsModel clip: true width: parent.width - height: Screen.width / 2 + height: Constants.libraryDelegatePosterHeight orientation: ListView.Horizontal spacing: Theme.paddingLarge leftMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin delegate: LibraryItemDelegate { poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags.primary, "Primary", {"maxHeight": height}) + blurhash: model.imageBlurHashes["primary"][model.imageTags.primary] title: model.name onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.id}) } diff --git a/sailfish/qml/pages/itemdetails/VideoPage.qml b/sailfish/qml/pages/itemdetails/VideoPage.qml index 330117c..0d5af89 100644 --- a/sailfish/qml/pages/itemdetails/VideoPage.qml +++ b/sailfish/qml/pages/itemdetails/VideoPage.qml @@ -56,6 +56,7 @@ BaseDetailPage { width: parent.width imageSource: Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width}) imageAspectRatio: Constants.horizontalVideoAspectRatio + imageBlurhash: itemData.imageBlurHashes["Primary"][itemData.imageTags["Primary"]] favourited: itemData.userData.isFavorite playProgress: itemData.userData.playedPercentage / 100 onPlayPressed: pageStack.push(Qt.resolvedUrl("../VideoPage.qml"),