1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-11-16 06:25:18 +00:00
harbour-sailfin/sailfish/qml/components/PlaybackBar.qml

628 lines
22 KiB
QML

/*
Sailfin: a Jellyfin client written using Qt
Copyright (C) 2021 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 QtMultimedia 5.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
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 === MediaPlayer.PlayingState
? "image://theme/icon-m-pause" : "image://theme/icon-m-play"
onClicked: manager.playbackState === MediaPlayer.PlayingState
? 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 }
}
}
]
}