/* 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 nl.netsoj.chris.Jellyfin 1.0 as J import "../components" import "../" /** * Main page, which simply shows some content of every library, as well as next items. */ Page { /// True if the models on this page already have been loaded and don't necessarily need a refresh property bool _modelsLoaded: false // Only for cover page readonly property string itemId: "" id: mainPage allowedOrientations: Orientation.All // This component is reused both in the normal state and error state Component { id: commonPullDownMenu PullDownMenu { MenuItem { //: Pulley menu item: navigate to application settings page text: qsTr("Settings") onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml")) } MenuItem { //: Pulley menu item: reload items on page text: qsTr("Reload") onClicked: loadModels(true) } busy: mediaLibraryLoader.status === J.ModelStatus.Loading } } SilicaFlickable { id: flickable anchors.fill: parent // PullDownMenu and PushUpMenu must be declared in SilicaFlickable, SilicaListView or SilicaGridView Loader { sourceComponent: commonPullDownMenu; } // Tell SilicaFlickable the height of its content. contentHeight: column.height + Theme.paddingMedium // Place our content in a Column. The PageHeader is always placed at the top // of the page, followed by our content. Column { id: column width: mainPage.width J.ItemModel { id: mediaLibraryModel loader: J.UsersViewsLoader { id: mediaLibraryLoader apiClient: appWindow.apiClient } } MoreSection { //- Section header for films and TV shows that an user hasn't completed yet. text: qsTr("Resume watching") clickable: false busy: userResumeLoader.status === J.ModelStatus.Loading Loader { width: parent.width sourceComponent: carrouselView property alias itemModel: userResumeModel property string collectionType: "series" J.ItemModel { id: userResumeModel loader: J.ResumeItemsLoader { id: userResumeLoader apiClient: appWindow.apiClient limit: 12 //recursive: true } } } } MoreSection { //- Section header for next episodes in a TV show that an user was watching. text: qsTr("Next up") clickable: false busy: showNextUpLoader.status === J.ModelStatus.Loading Loader { width: parent.width sourceComponent: carrouselView property alias itemModel: showNextUpModel property string collectionType: "series" J.ItemModel { id: showNextUpModel loader: J.NextUpLoader { id: showNextUpLoader apiClient: appWindow.apiClient enableUserData: true } } } } Repeater { model: mediaLibraryModel MoreSection { text: model.name busy: userItemModel.status !== J.UsersViewsLoader.Ready onHeaderClicked: appWindow.navigateToItem(model.jellyfinId, model.mediaType, model.type, model.isFolder); Loader { width: parent.width sourceComponent: carrouselView property alias itemModel: userItemModel property string collectionType: model.collectionType || "" J.ItemModel { id: userItemModel loader: J.LatestMediaLoader { apiClient: appWindow.apiClient parentId: jellyfinId } } Connections { target: mediaLibraryLoader onReady: userItemModel.reload() } } } } } } SilicaFlickable { id: errorFlickable anchors.fill: parent visible: false opacity: 0 Loader { sourceComponent: commonPullDownMenu; } ViewPlaceholder { enabled: true //- ViewPlaceholder text for when a network error has occurred and no contents could be loaded text: qsTr("Network error") //- ViewPlaceholder hint text for when a network error has occurred and no contents could be loaded. //- The user can reload the page by using the pulley, which this text should hint at. hintText: qsTr("Pull down to retry again") } } onStatusChanged: { if (status == PageStatus.Active) { appWindow.itemData = null } } Connections { target: appWindow.apiClient onAuthenticatedChanged: loadModels(false) } /** * Loads models if not laoded. Set force to true to reload models * even if loaded. */ function loadModels(force) { if (force || (appWindow.apiClient.authenticated && !_modelsLoaded)) { _modelsLoaded = true; mediaLibraryModel.reload() userResumeModel.reload() showNextUpModel.reload() } } Component { id: carrouselView SilicaListView { property bool isPortrait: Utils.usePortraitCover(collectionType) id: list clip: true height: { if (count > 0) { if (isPortrait) { Constants.libraryDelegatePosterHeight } else { Constants.libraryDelegateHeight } } else { 0 } } Behavior on height { NumberAnimation { easing.type: Easing.OutQuad; duration: 300 } } model: itemModel width: parent.width orientation: ListView.Horizontal leftMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin spacing: Theme.paddingLarge delegate: LibraryItemDelegate { property string id: model.jellyfinId title: model.name poster: Utils.itemModelImageUrl(appWindow.apiClient.baseUrl, model.jellyfinId, model.imageTags["Primary"], "Primary", {"maxHeight": height}) Binding on blurhash { when: poster !== "" value: model.imageBlurHashes["Primary"][model.imageTags["Primary"]] } landscape: !isPortrait progress: (typeof model.userDataPlayedProgress !== 0.0) ? model.userDataPlayedPercentage / 100 : 0.0 onClicked: { appWindow.navigateToItem(model.jellyfinId, model.mediaType, model.type, model.isFolder); } } } } state: "default" states: [ State { name: "default" when: mediaLibraryLoader.status !== J.UsersViewsLoader.Error }, State { name: "error" when: mediaLibraryLoader.status === J.UsersViewsLoader.Error PropertyChanges { target: errorFlickable visible: true opacity: 1 } PropertyChanges { target: flickable opacity: 0 visible: false } } ] transitions: [ Transition { from: "default" to: "error" SequentialAnimation { PropertyAction { target: errorFlickable; property: "visible"; value: true } FadeAnimation {} PropertyAction { target: flickable; property: "visible"; value: false } } }, Transition { from: "error" to: "default" SequentialAnimation { PropertyAction { target: flickable; property: "visible"; value: true } FadeAnimation {} PropertyAction { target: errorFlickable; property: "visible"; value: false } } } ] }