1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-07-06 06:33:51 +00:00
harbour-sailfin/sailfish/qml/components/PlaybackBar.qml
Chris Josten 57b67292fd sailfish: add LiveTvChannels page
This ocmmit adds a LiveTvChannels page for displaying the programs that
are now playing.

The section Live TV Channels on the main page now shows the TV channel
list in order of the channel number.

Additionally, it fixes an issue in ApiModel, where it would not reload
when a new loader was assigned. This is now fixed and some code on pages
that worked around this fix has been removed.
2024-06-03 22:16:13 +02:00

627 lines
22 KiB
QML

/*
Sailfin: a Jellyfin client written using Qt
Copyright (C) 2021-2024 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.6
import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J
import "../"
/**
*
* +---+--------------------------------------+
* |\ /| +---+ |
* | \ / | Media title | | |
* | X | | ⏸︎ | |
* | / \ | Artist 1, artist 2 | | |
* |/ \| +---+ |
* +-----+------------------------------------+
*/
PanelBackground {
id: playbackBar
height: Theme.itemSizeLarge
width: parent.width
y: parent.height - height
//FIXME: Once QTBUG-10822 is resolved, change to J.PlaybackManager
property var manager
property bool open
property real visibleSize: height
property bool isFullPage: false
property bool showQueue: false
property bool _pageWasShowingNavigationIndicator
readonly property bool _isItemSet: manager.item !== null && manager.item !== undefined && manager.item.jellyfinId.length > 0
readonly property bool controllingRemote: !manager.controllingSessionLocal
readonly property bool mediaLoading: [J.MediaStatus.Loading, J.MediaStatus.Buffering].indexOf(manager.mediaStatus) >= 0
transform: Translate {id: playbackBarTranslate; y: 0}
BackgroundItem {
id: backgroundItem
width: parent.width
height: parent.height
onClicked: playbackBar.state = "large"
RemoteImage {
id: albumArt
anchors {
left: parent.left
bottom: parent.bottom
top: parent.top
}
width: height
blurhash: {
if (_isItemSet && "Primary" in manager.item.imageBlurHashes && "Primary" in manager.item.imageTags) {
return manager.item.imageBlurHashes["Primary"][manager.item.imageTags["Primary"]]
} else {
return ""
}
}
source: largeAlbumArt.source
fillMode: Image.PreserveAspectCrop
opacity: 1
Image {
id: largeAlbumArt
source: Utils.itemImageUrl(apiClient.baseUrl, manager.item, "Primary")
fillMode: Image.PreserveAspectFit
anchors.fill: parent
opacity: 0
Behavior on opacity { FadeAnimation {} }
}
}
Rectangle {
id: playQueueShim
anchors.fill: albumArt
color: Theme.rgba(Theme.overlayBackgroundColor, Theme.opacityOverlay)
opacity: 0
}
Loader {
id: queueLoader
source: Qt.resolvedUrl("PlayQueue.qml")
anchors.fill: albumArt
active: false
visible: false
opacity: 0
Binding {
when: queueLoader.item !== null
target: queueLoader.item
property: "model"
value: manager.queue
//currentIndex: manager.queueIndex
}
Binding {
when: queueLoader.item !== null
target: queueLoader.item
property: "playbackManager"
value: manager
}
}
Column {
id: artistInfo
height: Theme.fontSizeMedium + Theme.fontSizeLarge
anchors {
left: albumArt.right
leftMargin: Theme.paddingMedium
right: playButton.left
verticalCenter: parent.verticalCenter
}
Label {
id: name
text: manager.item.jellyfinId
? manager.item.name
//: Shown in a bright font when no media is playing in the bottom bar and now playing screen
: qsTr("Nothing is playing")
width: Math.min(contentWidth, parent.width)
font.pixelSize: Theme.fontSizeMedium
maximumLineCount: 1
truncationMode: TruncationMode.Fade
}
Label {
id: artists
leftPadding: controllingRemote ? remoteIcon.width + Theme.paddingSmall : 0
text: {
if (!_isItemSet) {
if (controllingRemote) {
//: Shown when no media is being played, but the app is controlling another Jellyfin client
//: %1 is the name of said client
return qsTr("Connected to %1").arg(manager.controllingSessionName)
} else {
return qsTr("Start playing some media!")
}
}
var remoteText = "";
if (controllingRemote) {
remoteText = manager.controllingSessionName + " - "
}
switch(manager.item.mediaType) {
case "Audio":
var links = [];
var items = manager.item.artistItems;
console.log(items)
for (var i = 0; i < items.length; i++) {
links.push("<a href=\"%1\" style=\"text-decoration:none;color:%3\">%2</a>"
.arg(items[i].jellyfinId)
.arg(items[i].name)
.arg(Theme.secondaryColor)
)
}
return remoteText + links.join(", ")
}
return qsTr("No audio")
}
width: Math.min(contentWidth, parent.width)
font.pixelSize: Theme.fontSizeSmall
maximumLineCount: 1
truncationMode: TruncationMode.Fade
color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor
linkColor: Theme.secondaryColor
onLinkActivated: {
appWindow.navigateToItem(link, "Audio", "MusicArtist", true)
}
textFormat: Text.StyledText
Icon {
id: remoteIcon
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
}
height: parent.height
source: "image://theme/icon-s-device-upload"
visible: controllingRemote
}
}
}
IconButton {
id: playModeButton
anchors {
right: previousButton.left
rightMargin: Theme.paddingLarge
verticalCenter: playButton.verticalCenter
}
icon.source: "image://theme/icon-m-shuffle"
opacity: 0
enabled: false
onClicked: Notices.show(qsTr("Shuffle not yet implemented"))
}
IconButton {
id: previousButton
anchors {
right: playButton.left
rightMargin: Theme.paddingLarge
verticalCenter: playButton.verticalCenter
}
icon.source: "image://theme/icon-m-previous"
enabled: false
opacity: 0
onClicked: manager.previous()
}
IconButton {
id: playButton
anchors {
right: parent.right
rightMargin: Theme.paddingMedium
verticalCenter: parent.verticalCenter
}
icon.source: manager.playbackState === J.PlayerState.Playing
? "image://theme/icon-m-pause" : "image://theme/icon-m-play"
onClicked: manager.playbackState === J.PlayerState.Playing
? manager.pause()
: manager.play()
}
IconButton {
id: nextButton
anchors {
left: playButton.right
leftMargin: Theme.paddingLarge
verticalCenter: playButton.verticalCenter
}
icon.source: "image://theme/icon-m-next"
enabled: false
opacity: 0
onClicked: manager.next()
}
IconButton {
id: queueButton
anchors {
left: nextButton.right
leftMargin: Theme.paddingLarge
verticalCenter: playButton.verticalCenter
}
icon.source: "image://theme/icon-m-menu"
icon.highlighted: showQueue
enabled: false
opacity: 0
onClicked: showQueue = !showQueue
}
ProgressBar {
id: progressBar
anchors.verticalCenter: parent.top
width: parent.width
leftMargin: Theme.itemSizeLarge
rightMargin: 0
minimumValue: 0
value: manager.position
maximumValue: manager.duration
indeterminate: mediaLoading
}
Slider {
id: seekBar
animateValue: false
anchors.verticalCenter: progressBar.top
minimumValue: 0
value: manager.position
maximumValue: manager.duration
width: parent.width
stepSize: 1000
valueText: Utils.timeToText(value)
enabled: false
visible: false
onDownChanged: { if (!down) {
manager.seek(value);
// For some reason, the binding breaks when dragging the slider.
value = Qt.binding(function() { return manager.position})
}
}
}
}
states: [
State {
name: ""
// Show the bar whenever:
// 1. Either one of the following is true:
// a. The playbackmanager is playing media
// b. The playbackmanager is controlling a remote session
// AND
// 2. The playback bar isn't in the full page state
// AND
// 3. The topmost page on the pagestack hasn't requested to hide the page
when: (manager.playbackState !== J.PlayerState.Stopped || !manager.controllingSessionLocal)
&& !isFullPage && !("__hidePlaybackBar" in pageStack.currentPage)
},
State {
name: "large"
PropertyChanges {
target: playbackBar
height: pageStack.currentOrientation & Orientation.LandscapeMask ? Screen.width : Screen.height
}
PropertyChanges {
target: albumArt
forceBlurhash: true
width: parent.width
anchors.bottomMargin: Theme.paddingLarge
}
AnchorChanges {
target: albumArt
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: artistInfo.top
}
PropertyChanges {
target: artistInfo
anchors.leftMargin: Theme.horizontalPageMargin
anchors.rightMargin: Theme.horizontalPageMargin
anchors.bottomMargin: Theme.paddingLarge + seekBar.height - progressBar.height
height: Theme.fontSizeLarge + Theme.fontSizeMedium
}
AnchorChanges {
target: artistInfo
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: progressBar.top
anchors.verticalCenter: undefined
}
PropertyChanges {
target: progressBar
leftMargin: Screen.width / 8
rightMargin: Screen.width / 8
anchors.bottomMargin: Theme.paddingLarge
opacity: 0
visible: false
}
AnchorChanges {
target: progressBar
anchors.verticalCenter: undefined
anchors.bottom: playButton.top
}
PropertyChanges {
target: playButton
anchors.bottomMargin: Theme.paddingLarge * 2
/*icon.source: appWindow.mediaPlayer.playbackState === MediaPlayer.PlayingState
? "image://theme/icon-l-pause" : "image://theme/icon-l-play"*/
width: Theme.itemSizeMedium
height: Theme.itemSizeMedium
icon.width: icon.implicitWidth * 1.5
icon.height: icon.implicitWidth * 1.5
}
AnchorChanges {
target: playButton
anchors.right: undefined
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: undefined
anchors.bottom: parent.bottom
}
PropertyChanges {
target: previousButton; opacity: 1; enabled: playbackManager.hasPrevious;
}
PropertyChanges {
target: nextButton; opacity: 1; enabled: playbackManager.hasNext;
}
PropertyChanges {
target: playModeButton; opacity: 1; enabled: true;
}
PropertyChanges {
target: queueButton; opacity: 1; enabled: true;
}
PropertyChanges {
target: seekBar
enabled: true
visible: true
animateValue: true
}
PropertyChanges {
target: largeAlbumArt
opacity: status == Image.Ready ? 1.0 : 0.0
}
PropertyChanges {
target: artists
font.pixelSize: Theme.fontSizeMedium
}
AnchorChanges {
target: artists
anchors.horizontalCenter: parent.horizontalCenter
}
PropertyChanges {
target: name
font.pixelSize: Theme.fontSizeLarge
}
AnchorChanges {
target: name
anchors.horizontalCenter: parent.horizontalCenter
}
PropertyChanges {
target: backgroundItem
enabled: false
}
},
State {
name: "hidden"
when: ((manager.playbackState === J.PlayerState.Stopped && !mediaLoading)
|| ("__hidePlaybackBar" in pageStack.currentPage && pageStack.currentPage.__hidePlaybackBar))
&& !isFullPage
PropertyChanges {
target: playbackBarTranslate
// + small padding since the ProgressBar otherwise would stick out
y: playbackBar.height + Theme.paddingMedium
}
PropertyChanges {
target: playbackBar
visibleSize: 0
}
PropertyChanges {
target: albumArt
source: ""
}
},
State {
name: "page"
when: isFullPage && !showQueue
extend: "large"
PropertyChanges {
target: queueLoader
active: true
}
},
State {
name: "pageQueue"
when: isFullPage && showQueue
extend: "page"
PropertyChanges {
target: queueLoader
visible: true
opacity: 1
}
PropertyChanges {
target: largeAlbumArt
opacity: 0
visible: false
}
PropertyChanges {
target: playQueueShim
opacity: 1
}
}
]
Component {
id: fullPage
Page {
property bool __hidePlaybackBar: true
property bool __isPlaybackBar: true
showNavigationIndicator: true
allowedOrientations: appWindow.allowedOrientations
SilicaFlickable {
anchors.fill: parent
PullDownMenu {
/*MenuItem {
//: Pulley menu item to view detailed media information of a song
text: qsTr("Info")
}
MenuItem {
//: Pulley menu item: add music to a playlist
text: qsTr("Add to playlist")
}*/
MenuItem {
//: Pulley menu item: stops playback of music
text: qsTr("Stop")
onClicked: {
playbackManager.stop()
pageStack.pop()
}
}
}
Loader {
Component.onCompleted: setSource(Qt.resolvedUrl("PlaybackBar.qml"),
{"isFullPage": true, "manager": manager, "y": 0})
anchors.fill: parent
}
}
}
}
transitions: [
Transition {
from: ""
to: "large"
// We animate this object to a large size and then quickly swap out this component
// with a page containing this component.
SequentialAnimation {
ScriptAction {
script: {
_pageWasShowingNavigationIndicator = pageStack.currentPage.showNavigationIndicator
appWindow.pageStack.currentPage.showNavigationIndicator = false
seekBar.enabled = true
seekBar.visible = true
}
}
ParallelAnimation {
NumberAnimation {
properties: "width,height,targetX,targetY,leftMargin,rightMargin,font.pixelSize"
easing.type: Easing.OutCubic
duration: 300 // Long, but avoids stutters
}
AnchorAnimation {
easing.type: Easing.OutCubic
duration: 300
}
FadeAnimation {
duration: 300
}
}
ScriptAction {
script: {
pageStack.currentPage.showNavigationIndicator = _pageWasShowingNavigationIndicator
pageStack.push(fullPage, {"background": pageStack.currentPage.background}, PageStackAction.Immediate)
/*playbackBarTranslate.y = playbackBar.height + Theme.paddingMedium
appWindow.bottomMargin = 0*/
}
}
}
},
Transition {
from: "*"
to: "page"
},
Transition {
from: "hidden"
to: ""
NumberAnimation {
targets: [playbackBarTranslate, playbackBar]
properties: "y,visibleSize"
duration: 250
easing.type: Easing.OutQuad
}
NumberAnimation {
target: appWindow
property: "bottomMargin"
duration: 250
to: Theme.itemSizeLarge
easing.type: Easing.OutQuad
}
},
Transition {
from: ""
to: "hidden"
SequentialAnimation {
ParallelAnimation {
NumberAnimation {
targets: [playbackBarTranslate, playbackBar]
properties: "y,visibleSize"
duration: 250
easing.type: Easing.OutQuad
}
NumberAnimation {
target: appWindow
property: "bottomMargin"
duration: 250
to: 0
easing.type: Easing.OutQuad
}
}
}
},
Transition {
from: "page"
to: "pageQueue"
reversible: false
SequentialAnimation {
FadeAnimation {
targets: [playQueueShim, largeAlbumArt, queueLoader]
property: "opacity"
}
PropertyAction { target: largeAlbumArt; property: "visible"; value: false }
}
},
Transition {
from: "pageQueue"
to: "page"
reversible: false
SequentialAnimation {
PropertyAction { target: largeAlbumArt; property: "visible"; value: true }
PropertyAction { target: largeAlbumArt; property: "opacity"; value: 0 }
FadeAnimation {
targets: [playQueueShim, largeAlbumArt, queueLoader]
property: "opacity"
}
PropertyAction { target: queueLoader; property: "visible"; value: false }
}
}
]
}