mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2025-09-01 08:52:45 +00:00
Moved playback logic to C++-side (and refractoring)
This commit is contained in:
parent
895731ae38
commit
f7bca333c8
35 changed files with 1063 additions and 449 deletions
|
@ -30,7 +30,8 @@ set(sailfin_QML_SOURCES
|
|||
qml/components/MoreSection.qml
|
||||
qml/components/PlainLabel.qml
|
||||
qml/components/PlaybackBar.qml
|
||||
qml/components/PlayToolbar.qml
|
||||
qml/components/PlayQueue.qml
|
||||
qml/components/PlayToolbar.qml
|
||||
qml/components/RemoteImage.qml
|
||||
qml/components/Shim.qml
|
||||
qml/components/UserGridDelegate.qml
|
||||
|
@ -65,6 +66,8 @@ target_link_libraries(harbour-sailfin PRIVATE Qt5::Gui Qt5::Qml Qt5::Quick Sailf
|
|||
# Note: this may break when the compiler changes. -rdynamic and -pie seem to be needed for the
|
||||
# invoker/booster to work
|
||||
jellyfin-qt "-Wl,-rpath,${CMAKE_INSTALL_LIBDIR} -rdynamic -pie")
|
||||
target_compile_definitions(harbour-sailfin
|
||||
PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>)
|
||||
|
||||
install(TARGETS harbour-sailfin
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
|
@ -107,5 +110,5 @@ install(FILES icons/172x172/harbour-sailfin.png
|
|||
# format.
|
||||
file(WRITE "${CMAKE_BINARY_DIR}/QtCreatorDeployment.txt"
|
||||
"${CMAKE_INSTALL_PREFIX}
|
||||
sailfish/harbour-sailfin:bin
|
||||
${CMAKE_BINARY_DIR}/sailfish/harbour-sailfin:bin
|
||||
")
|
||||
|
|
|
@ -48,7 +48,7 @@ function ticksToText(ticks, showHours) {
|
|||
}
|
||||
|
||||
function itemImageUrl(baseUrl, item, type, options) {
|
||||
if (!item.imageTags[type]) { return "" }
|
||||
if (item === null || !item.imageTags[type]) { return "" }
|
||||
return itemModelImageUrl(baseUrl, item.jellyfinId, item.imageTags[type], type, options)
|
||||
}
|
||||
|
||||
|
|
16
sailfish/qml/components/PlayQueue.qml
Normal file
16
sailfish/qml/components/PlayQueue.qml
Normal file
|
@ -0,0 +1,16 @@
|
|||
import QtQuick 2.6
|
||||
import Sailfish.Silica 1.0
|
||||
|
||||
import nl.netsoj.chris.Jellyfin 1.0
|
||||
|
||||
import "music"
|
||||
|
||||
SilicaListView {
|
||||
header: SectionHeader { text: qsTr("Play queue") }
|
||||
delegate: SongDelegate {
|
||||
artists: model.artists
|
||||
name: model.name
|
||||
width: parent.width
|
||||
indexNumber: ListView.index
|
||||
}
|
||||
}
|
|
@ -43,6 +43,8 @@ PanelBackground {
|
|||
property PlaybackManager manager
|
||||
property bool open
|
||||
property real visibleSize: height
|
||||
property bool isFullPage: false
|
||||
property bool showQueue: false
|
||||
|
||||
property bool _pageWasShowingNavigationIndicator
|
||||
|
||||
|
@ -53,7 +55,7 @@ PanelBackground {
|
|||
id: backgroundItem
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
onClicked: playbackBar.state = (playbackBar.state == "large" ? "open" : "large")
|
||||
onClicked: playbackBar.state = "large"
|
||||
|
||||
|
||||
RemoteImage {
|
||||
|
@ -64,7 +66,10 @@ PanelBackground {
|
|||
top: parent.top
|
||||
}
|
||||
width: height
|
||||
blurhash: manager.item.imageBlurHashes["Primary"][manager.item.imageTags["Primary"]]
|
||||
Binding on blurhash {
|
||||
when: manager.item !== null && "Primary" in manager.item.imageBlurHashes
|
||||
value: manager.item.imageBlurHashes["Primary"][manager.item.imageTags["Primary"]]
|
||||
}
|
||||
source: largeAlbumArt.source
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
|
||||
|
@ -77,6 +82,20 @@ PanelBackground {
|
|||
Behavior on opacity { FadeAnimation {} }
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: queueLoader
|
||||
source: Qt.resolvedUrl("PlayQueue.qml")
|
||||
anchors.fill: albumArt
|
||||
active: false
|
||||
visible: false
|
||||
Binding {
|
||||
when: queueLoader.item !== null
|
||||
target: queueLoader.item
|
||||
property: "model"
|
||||
value: manager.queue
|
||||
//currentIndex: manager.queueIndex
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: artistInfo
|
||||
|
@ -106,6 +125,7 @@ PanelBackground {
|
|||
case "Audio":
|
||||
return manager.item.artists.join(", ")
|
||||
}
|
||||
return qsTr("Not audio")
|
||||
}
|
||||
width: Math.min(contentWidth, parent.width)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
|
@ -146,11 +166,11 @@ PanelBackground {
|
|||
rightMargin: Theme.paddingMedium
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
icon.source: appWindow.mediaPlayer.playbackState === MediaPlayer.PlayingState
|
||||
icon.source: manager.playbackState === MediaPlayer.PlayingState
|
||||
? "image://theme/icon-m-pause" : "image://theme/icon-m-play"
|
||||
onClicked: appWindow.mediaPlayer.playbackState === MediaPlayer.PlayingState
|
||||
? appWindow.mediaPlayer.pause()
|
||||
: appWindow.mediaPlayer.play()
|
||||
onClicked: manager.playbackState === MediaPlayer.PlayingState
|
||||
? manager.pause()
|
||||
: manager.play()
|
||||
}
|
||||
IconButton {
|
||||
id: nextButton
|
||||
|
@ -171,8 +191,10 @@ PanelBackground {
|
|||
verticalCenter: playButton.verticalCenter
|
||||
}
|
||||
icon.source: "image://theme/icon-m-menu"
|
||||
icon.highlighted: showQueue
|
||||
enabled: false
|
||||
opacity: 0
|
||||
onClicked: showQueue = !showQueue
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
|
@ -182,9 +204,9 @@ PanelBackground {
|
|||
leftMargin: Theme.itemSizeLarge
|
||||
rightMargin: 0
|
||||
minimumValue: 0
|
||||
value: appWindow.mediaPlayer.position
|
||||
maximumValue: appWindow.mediaPlayer.duration
|
||||
indeterminate: [MediaPlayer.Loading, MediaPlayer.Buffering].indexOf(appWindow.mediaPlayer.status) >= 0
|
||||
value: manager.position
|
||||
maximumValue: manager.duration
|
||||
indeterminate: [MediaPlayer.Loading, MediaPlayer.Buffering].indexOf(manager.mediaStatus) >= 0
|
||||
}
|
||||
|
||||
Slider {
|
||||
|
@ -192,17 +214,17 @@ PanelBackground {
|
|||
animateValue: false
|
||||
anchors.verticalCenter: progressBar.top
|
||||
minimumValue: 0
|
||||
value: appWindow.mediaPlayer.position
|
||||
maximumValue: appWindow.mediaPlayer.duration
|
||||
value: manager.position
|
||||
maximumValue: manager.duration
|
||||
width: parent.width
|
||||
stepSize: 1000
|
||||
valueText: Utils.timeToText(value)
|
||||
enabled: false
|
||||
visible: false
|
||||
onDownChanged: { if (!down) {
|
||||
appWindow.mediaPlayer.seek(value);
|
||||
manager.seek(value);
|
||||
// For some reason, the binding breaks when dragging the slider.
|
||||
value = Qt.binding(function() { return appWindow.mediaPlayer.position})
|
||||
value = Qt.binding(function() { return manager.position})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -212,7 +234,7 @@ PanelBackground {
|
|||
states: [
|
||||
State {
|
||||
name: ""
|
||||
when: appWindow.mediaPlayer.playbackState !== MediaPlayer.StoppedState && state != "page" && !("__hidePlaybackBar" in pageStack.currentPage)
|
||||
when: manager.playbackState !== MediaPlayer.StoppedState && !isFullPage && !("__hidePlaybackBar" in pageStack.currentPage)
|
||||
},
|
||||
State {
|
||||
name: "large"
|
||||
|
@ -328,27 +350,46 @@ PanelBackground {
|
|||
}
|
||||
|
||||
},
|
||||
State {
|
||||
name: "hidden"
|
||||
when: (appWindow.mediaPlayer.playbackState === MediaPlayer.StoppedState || "__hidePlaybackBar" in pageStack.currentPage) && state != "page"
|
||||
PropertyChanges {
|
||||
target: playbackBarTranslate
|
||||
// + small padding since the ProgressBar otherwise would stick out
|
||||
y: playbackBar.height + Theme.paddingSmall
|
||||
}
|
||||
PropertyChanges {
|
||||
target: playbackBar
|
||||
visibleSize: 0
|
||||
}
|
||||
PropertyChanges {
|
||||
target: albumArt
|
||||
source: ""
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "page"
|
||||
extend: "large"
|
||||
}
|
||||
State {
|
||||
name: "hidden"
|
||||
when: (manager.playbackState === MediaPlayer.StoppedState || "__hidePlaybackBar" in pageStack.currentPage) && !isFullPage
|
||||
PropertyChanges {
|
||||
target: playbackBarTranslate
|
||||
// + small padding since the ProgressBar otherwise would stick out
|
||||
y: playbackBar.height + Theme.paddingSmall
|
||||
}
|
||||
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
|
||||
}
|
||||
PropertyChanges {
|
||||
target: largeAlbumArt
|
||||
opacity: 0
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Component {
|
||||
|
@ -371,7 +412,7 @@ PanelBackground {
|
|||
}
|
||||
Loader {
|
||||
Component.onCompleted: setSource(Qt.resolvedUrl("PlaybackBar.qml"),
|
||||
{"state": "page", "manager": manager, "y": 0})
|
||||
{"isFullPage": true, "manager": manager, "y": 0})
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
|
@ -421,23 +462,19 @@ PanelBackground {
|
|||
},
|
||||
Transition {
|
||||
from: "hidden"
|
||||
SequentialAnimation {
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
targets: [playbackBarTranslate, playbackBar]
|
||||
properties: "y,visibileSize"
|
||||
duration: 250
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
targets: [playbackBarTranslate, playbackBar]
|
||||
properties: "y,visibileSize"
|
||||
duration: 250
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: appWindow
|
||||
property: "bottomMargin"
|
||||
duration: 250
|
||||
to: Theme.itemSizeLarge
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
NumberAnimation {
|
||||
target: appWindow
|
||||
property: "bottomMargin"
|
||||
duration: 250
|
||||
to: Theme.itemSizeLarge
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
|
|
|
@ -36,10 +36,10 @@ SilicaItem {
|
|||
property bool resume
|
||||
property int progress
|
||||
readonly property bool landscape: videoOutput.contentRect.width > videoOutput.contentRect.height
|
||||
property MediaPlayer player
|
||||
readonly property bool hudVisible: !hud.hidden || player.error !== MediaPlayer.NoError
|
||||
readonly property bool hudVisible: !hud.hidden || manager.error !== MediaPlayer.NoError
|
||||
property int audioTrack: 0
|
||||
property int subtitleTrack: 0
|
||||
property PlaybackManager manager;
|
||||
|
||||
// Blackground to prevent the ambience from leaking through
|
||||
Rectangle {
|
||||
|
@ -49,27 +49,27 @@ SilicaItem {
|
|||
|
||||
VideoOutput {
|
||||
id: videoOutput
|
||||
source: player
|
||||
source: manager
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
VideoHud {
|
||||
id: hud
|
||||
anchors.fill: parent
|
||||
player: playerRoot.player
|
||||
manager: playerRoot.manager
|
||||
title: videoPlayer.title
|
||||
|
||||
Label {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.horizontalPageMargin
|
||||
text: item.jellyfinId + "\n" + appWindow.playbackManager.streamUrl + "\n"
|
||||
+ (appWindow.playbackManager.playMethod == PlaybackManager.DirectPlay ? "Direct Play" : "Transcoding") + "\n"
|
||||
+ player.position + "\n"
|
||||
+ player.status + "\n"
|
||||
+ player.bufferProgress + "\n"
|
||||
+ player.metaData.videoCodec + "@" + player.metaData.videoFrameRate + "(" + player.metaData.videoBitRate + ")" + "\n"
|
||||
+ player.metaData.audioCodec + "(" + player.metaData.audioBitRate + ")" + "\n"
|
||||
+ player.errorString + "\n"
|
||||
+ (manager.playMethod === PlaybackManager.DirectPlay ? "Direct Play" : "Transcoding") + "\n"
|
||||
+ manager.position + "\n"
|
||||
+ manager.mediaStatus + "\n"
|
||||
// + player.bufferProgress + "\n"
|
||||
// + player.metaData.videoCodec + "@" + player.metaData.videoFrameRate + "(" + player.metaData.videoBitRate + ")" + "\n"
|
||||
// + player.metaData.audioCodec + "(" + player.metaData.audioBitRate + ")" + "\n"
|
||||
// + player.errorString + "\n"
|
||||
font.pixelSize: Theme.fontSizeExtraSmall
|
||||
wrapMode: "WordWrap"
|
||||
visible: appWindow.showDebugInfo
|
||||
|
@ -78,17 +78,17 @@ SilicaItem {
|
|||
|
||||
VideoError {
|
||||
anchors.fill: videoOutput
|
||||
player: playerRoot.player
|
||||
player: manager
|
||||
}
|
||||
|
||||
function start() {
|
||||
appWindow.playbackManager.audioIndex = audioTrack
|
||||
appWindow.playbackManager.subtitleIndex = subtitleTrack
|
||||
appWindow.playbackManager.resumePlayback = resume
|
||||
appWindow.playbackManager.item = item
|
||||
manager.audioIndex = audioTrack
|
||||
manager.subtitleIndex = subtitleTrack
|
||||
manager.resumePlayback = resume
|
||||
manager.playItem(item.jellyfinId)
|
||||
}
|
||||
|
||||
function stop() {
|
||||
player.stop()
|
||||
manager.stop();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,9 +20,11 @@ import QtQuick 2.6
|
|||
import Sailfish.Silica 1.0
|
||||
import QtMultimedia 5.6
|
||||
|
||||
import nl.netsoj.chris.Jellyfin 1.0
|
||||
|
||||
Rectangle {
|
||||
id: videoError
|
||||
property MediaPlayer player
|
||||
property PlaybackManager player
|
||||
color: pal.palette.overlayBackgroundColor
|
||||
opacity: player.error === MediaPlayer.NoError ? 0.0 : 1.0
|
||||
Behavior on opacity { FadeAnimator {} }
|
||||
|
|
|
@ -20,6 +20,8 @@ import QtQuick 2.6
|
|||
import QtMultimedia 5.6
|
||||
import Sailfish.Silica 1.0
|
||||
|
||||
import nl.netsoj.chris.Jellyfin 1.0
|
||||
|
||||
import "../../Utils.js" as Utils
|
||||
|
||||
/**
|
||||
|
@ -28,7 +30,7 @@ import "../../Utils.js" as Utils
|
|||
*/
|
||||
Item {
|
||||
id: videoHud
|
||||
property MediaPlayer player
|
||||
property PlaybackManager manager
|
||||
property string title
|
||||
property bool _manuallyActivated: false
|
||||
readonly property bool hidden: opacity == 0.0
|
||||
|
@ -76,19 +78,19 @@ Item {
|
|||
id: busyIndicator
|
||||
anchors.centerIn: parent
|
||||
size: BusyIndicatorSize.Medium
|
||||
running: [MediaPlayer.Loading, MediaPlayer.Stalled].indexOf(player.status) >= 0
|
||||
running: [MediaPlayer.Loading, MediaPlayer.Stalled].indexOf(manager.mediaStatus) >= 0
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: playPause
|
||||
enabled: !hidden
|
||||
anchors.centerIn: parent
|
||||
icon.source: player.playbackState == MediaPlayer.PausedState ? "image://theme/icon-l-play" : "image://theme/icon-l-pause"
|
||||
icon.source: manager.playbackState === MediaPlayer.PausedState ? "image://theme/icon-l-play" : "image://theme/icon-l-pause"
|
||||
onClicked: {
|
||||
if (player.playbackState == MediaPlayer.PlayingState) {
|
||||
player.pause()
|
||||
if (manager.playbackState === MediaPlayer.PlayingState) {
|
||||
manager.pause()
|
||||
} else {
|
||||
player.play()
|
||||
manager.play()
|
||||
}
|
||||
}
|
||||
visible: !busyIndicator.running
|
||||
|
@ -99,7 +101,7 @@ Item {
|
|||
anchors.bottom: parent.bottom
|
||||
width: parent.width
|
||||
height: progress.height
|
||||
visible: [MediaPlayer.Unavailable, MediaPlayer.Loading, MediaPlayer.NoMedia].indexOf(player.status) == -1
|
||||
visible: [MediaPlayer.Unavailable, MediaPlayer.Loading, MediaPlayer.NoMedia].indexOf(manager.mediaStatus) == -1
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: Theme.rgba(palette.overlayBackgroundColor, 0.15); }
|
||||
|
@ -116,19 +118,19 @@ Item {
|
|||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.horizontalPageMargin
|
||||
anchors.verticalCenter: progressSlider.verticalCenter
|
||||
text: Utils.timeToText(player.position)
|
||||
text: Utils.timeToText(manager.position)
|
||||
}
|
||||
|
||||
Slider {
|
||||
id: progressSlider
|
||||
enabled: player.seekable
|
||||
value: player.position
|
||||
maximumValue: player.duration
|
||||
enabled: manager.seekable
|
||||
value: manager.position
|
||||
maximumValue: manager.duration
|
||||
stepSize: 1000
|
||||
anchors.left: playedTime.right
|
||||
anchors.right: totalTime.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onDownChanged: if (!down) { player.seek(value) }
|
||||
onDownChanged: if (!down) { manager.seek(value) }
|
||||
}
|
||||
|
||||
Label {
|
||||
|
@ -136,7 +138,7 @@ Item {
|
|||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.horizontalPageMargin
|
||||
anchors.verticalCenter: progress.verticalCenter
|
||||
text: Utils.timeToText(player.duration)
|
||||
text: Utils.timeToText(manager.duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -144,10 +146,10 @@ Item {
|
|||
|
||||
|
||||
Connections {
|
||||
target: player
|
||||
onStatusChanged: {
|
||||
console.log("New mediaPlayer status: " + player.status)
|
||||
switch(player.status) {
|
||||
target: manager
|
||||
onMediaStatusChanged: {
|
||||
console.log("New mediaPlayer status: " + manager.mediaStatus)
|
||||
switch(manager.mediaStatus) {
|
||||
case MediaPlayer.Loaded:
|
||||
case MediaPlayer.Buffering:
|
||||
show(false)
|
||||
|
|
|
@ -32,7 +32,7 @@ ApplicationWindow {
|
|||
id: appWindow
|
||||
property bool _hasInitialized: false
|
||||
// The global mediaPlayer instance
|
||||
readonly property MediaPlayer mediaPlayer: _mediaPlayer
|
||||
//readonly property MediaPlayer mediaPlayer: _mediaPlayer
|
||||
readonly property PlaybackManager playbackManager: _playbackManager
|
||||
|
||||
// Data of the currently selected item. For use on the cover.
|
||||
|
@ -41,7 +41,7 @@ ApplicationWindow {
|
|||
property string collectionId
|
||||
|
||||
// Bad way to implement settings, but it'll do for now.
|
||||
property bool showDebugInfo: false
|
||||
property bool showDebugInfo: true
|
||||
|
||||
property bool _hidePlaybackBar: false
|
||||
|
||||
|
@ -65,13 +65,13 @@ ApplicationWindow {
|
|||
}
|
||||
}
|
||||
cover: {
|
||||
if ([MediaPlayer.NoMedia, MediaPlayer.InvalidMedia, MediaPlayer.UnknownStatus].indexOf(mediaPlayer.status) >= 0) {
|
||||
if ([MediaPlayer.NoMedia, MediaPlayer.InvalidMedia, MediaPlayer.UnknownStatus].indexOf(playbackManager.status) >= 0) {
|
||||
if (itemData) {
|
||||
return Qt.resolvedUrl("cover/PosterCover.qml")
|
||||
} else {
|
||||
return Qt.resolvedUrl("cover/CoverPage.qml")
|
||||
}
|
||||
} else if (mediaPlayer.hasVideo){
|
||||
} else if (playbackManager.hasVideo){
|
||||
return Qt.resolvedUrl("cover/VideoCover.qml")
|
||||
}
|
||||
}
|
||||
|
@ -87,26 +87,25 @@ ApplicationWindow {
|
|||
}
|
||||
}
|
||||
|
||||
MediaPlayer {
|
||||
/*MediaPlayer {
|
||||
id: _mediaPlayer
|
||||
autoPlay: true
|
||||
}
|
||||
}*/
|
||||
|
||||
PlaybackManager {
|
||||
id: _playbackManager
|
||||
apiClient: ApiClient
|
||||
mediaPlayer: _mediaPlayer
|
||||
audioIndex: 0
|
||||
autoOpen: true
|
||||
}
|
||||
|
||||
// Keep the sytem alive while playing media
|
||||
KeepAlive {
|
||||
enabled: _mediaPlayer.playbackState == MediaPlayer.PlayingState
|
||||
enabled: playbackManager.playbackState === MediaPlayer.PlayingState
|
||||
}
|
||||
|
||||
DisplayBlanking {
|
||||
preventBlanking: _mediaPlayer.playbackState == MediaPlayer.PlayingState && _mediaPlayer.hasVideo
|
||||
preventBlanking: playbackManager.playbackState === MediaPlayer.PlayingState && playbackManager.hasVideo
|
||||
}
|
||||
|
||||
PlaybackBar {
|
||||
|
|
|
@ -78,7 +78,7 @@ Page {
|
|||
//- Section header for films and TV shows that an user hasn't completed yet.
|
||||
text: qsTr("Resume watching")
|
||||
clickable: false
|
||||
busy: userResumeModel.status == ApiModel.Loading
|
||||
busy: userResumeModel.status === ApiModel.Loading
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: carrouselView
|
||||
|
@ -97,7 +97,7 @@ Page {
|
|||
//- Section header for next episodes in a TV show that an user was watching.
|
||||
text: qsTr("Next up")
|
||||
clickable: false
|
||||
busy: showNextUpModel.status == ApiModel.Loading
|
||||
busy: showNextUpModel.status === ApiModel.Loading
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
|
@ -121,9 +121,9 @@ Page {
|
|||
model: mediaLibraryModel
|
||||
MoreSection {
|
||||
text: model.name
|
||||
busy: userItemModel.status != ApiModel.Ready
|
||||
busy: userItemModel.status !== ApiModel.Ready
|
||||
|
||||
onHeaderClicked: pageStack.push(Qt.resolvedUrl("itemdetails/CollectionPage.qml"), {"itemId": model.id})
|
||||
onHeaderClicked: pageStack.push(Qt.resolvedUrl("itemdetails/CollectionPage.qml"), {"itemId": model.jellyfinId})
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: carrouselView
|
||||
|
@ -133,16 +133,12 @@ Page {
|
|||
UserItemLatestModel {
|
||||
id: userItemModel
|
||||
apiClient: ApiClient
|
||||
parentId: model.id
|
||||
parentId: jellyfinId
|
||||
limit: 16
|
||||
}
|
||||
Connections {
|
||||
target: mediaLibraryModel
|
||||
onStatusChanged: {
|
||||
if (status == ApiModel.Ready) {
|
||||
userItemModel.reload()
|
||||
}
|
||||
}
|
||||
onReady: userItemModel.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -154,7 +150,6 @@ Page {
|
|||
anchors.fill: parent
|
||||
visible: false
|
||||
opacity: 0
|
||||
contentHeight: errorColumn.height
|
||||
|
||||
Loader { sourceComponent: commonPullDownMenu; }
|
||||
|
||||
|
@ -220,15 +215,18 @@ Page {
|
|||
rightMargin: Theme.horizontalPageMargin
|
||||
spacing: Theme.paddingLarge
|
||||
delegate: LibraryItemDelegate {
|
||||
property string id: model.id
|
||||
property string id: model.jellyfinId
|
||||
title: model.name
|
||||
poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags["primary"], "Primary", {"maxHeight": height})
|
||||
blurhash: model.imageBlurHashes["primary"][model.imageTags["primary"]]
|
||||
poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.jellyfinId, model.imageTags["Primary"], "Primary", {"maxHeight": height})
|
||||
Binding on blurhash {
|
||||
when: poster !== ""
|
||||
value: model.imageBlurHashes["Primary"][model.imageTags["Primary"]]
|
||||
}
|
||||
landscape: !Utils.usePortraitCover(collectionType)
|
||||
progress: (typeof model.userData !== "undefined") ? model.userData.playedPercentage / 100 : 0.0
|
||||
|
||||
onClicked: {
|
||||
pageStack.push(Utils.getPageUrl(model.mediaType, model.type, model.isFolder), {"itemId": model.id})
|
||||
pageStack.push(Utils.getPageUrl(model.mediaType, model.type, model.isFolder), {"itemId": model.jellyfinId, "itemData": model.qtObject})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ Page {
|
|||
VideoPlayer {
|
||||
id: videoPlayer
|
||||
anchors.fill: parent
|
||||
player: appWindow.mediaPlayer
|
||||
manager: appWindow.playbackManager
|
||||
title: itemData.name
|
||||
audioTrack: videoPage.audioTrack
|
||||
subtitleTrack: videoPage.subtitleTrack
|
||||
|
@ -61,7 +61,7 @@ Page {
|
|||
|
||||
onStatusChanged: {
|
||||
switch(status) {
|
||||
case PageStatus.Inactive:
|
||||
case PageStatus.Deactivating:
|
||||
videoPlayer.stop()
|
||||
break;
|
||||
case PageStatus.Active:
|
||||
|
|
|
@ -88,8 +88,7 @@ Page {
|
|||
id: jItem
|
||||
apiClient: ApiClient
|
||||
onStatusChanged: {
|
||||
console.log("Status changed: " + newStatus, JSON.stringify(jItem))
|
||||
console.log(jItem.mediaStreams)
|
||||
//console.log("Status changed: " + newStatus, JSON.stringify(jItem))
|
||||
if (status == JellyfinItem.Ready) {
|
||||
updateBackdrop()
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ BaseDetailPage {
|
|||
anchors.fill: parent
|
||||
model: collectionModel
|
||||
cellWidth: Constants.libraryDelegateWidth
|
||||
cellHeight: Utils.usePortraitCover(itemData.type) ? Constants.libraryDelegatePosterHeight
|
||||
cellHeight: Utils.usePortraitCover(itemData.collectionType) ? Constants.libraryDelegatePosterHeight
|
||||
: Constants.libraryDelegateHeight
|
||||
visible: itemData.status !== JellyfinItem.Error
|
||||
|
||||
|
@ -54,14 +54,14 @@ BaseDetailPage {
|
|||
text: qsTr("Sort by")
|
||||
onClicked: pageStack.push(sortPageComponent)
|
||||
}
|
||||
busy: collectionModel.status == ApiModel.Loading
|
||||
busy: collectionModel.status === ApiModel.Loading
|
||||
}
|
||||
delegate: GridItem {
|
||||
RemoteImage {
|
||||
id: itemImage
|
||||
anchors.fill: parent
|
||||
source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags.primary, "Primary", {"maxWidth": width})
|
||||
blurhash: model.imageBlurHashes.primary[model.imageTags.primary]
|
||||
source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.jellyfinId, model.imageTags.Primary, "Primary", {"maxWidth": width})
|
||||
blurhash: model.imageBlurHashes.Primary[model.imageTags.Primary]
|
||||
fallbackColor: Utils.colorFromString(model.name)
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
clip: true
|
||||
|
@ -90,7 +90,7 @@ BaseDetailPage {
|
|||
horizontalAlignment: Text.AlignLeft
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type, model.isFolder), {"itemId": model.id})
|
||||
onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type, model.isFolder), {"itemId": model.jellyfinId})
|
||||
}
|
||||
|
||||
ViewPlaceholder {
|
||||
|
|
|
@ -29,7 +29,6 @@ import "../.."
|
|||
BaseDetailPage {
|
||||
id: albumPageRoot
|
||||
readonly property int _songIndexWidth: 100
|
||||
property string _albumArtistText: itemData.albumArtist
|
||||
width: 800 * Theme.pixelRatio
|
||||
|
||||
readonly property bool _twoColumns: albumPageRoot.width / Theme.pixelRatio >= 800
|
||||
|
@ -78,7 +77,7 @@ BaseDetailPage {
|
|||
artists: model.artists
|
||||
duration: model.runTimeTicks
|
||||
indexNumber: model.indexNumber
|
||||
onClicked: window.playbackManager.playItem(model.id)
|
||||
onClicked: window.playbackManager.playItem(model.jellyfinId)
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
|
@ -88,11 +87,6 @@ BaseDetailPage {
|
|||
Connections {
|
||||
target: itemData
|
||||
onAlbumArtistsChanged: {
|
||||
console.log(itemData.albumArtists)
|
||||
_albumArtistText = ""
|
||||
for (var i = 0; i < itemData.albumArtists.length; i++) {
|
||||
_albumArtistText += itemData.albumArtists[i]["name"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,7 +94,7 @@ BaseDetailPage {
|
|||
item.albumArt = Qt.binding(function(){ return Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width})})
|
||||
item.name = Qt.binding(function(){ return itemData.name})
|
||||
item.releaseYear = Qt.binding(function() { return itemData.productionYear})
|
||||
item.albumArtist = Qt.binding(function() { return _albumArtistText})
|
||||
item.albumArtist = Qt.binding(function() { return itemData.albumArtist})
|
||||
item.duration = Qt.binding(function() { return itemData.runTimeTicks})
|
||||
item.songCount = Qt.binding(function() { return itemData.childCount})
|
||||
item.listview = Qt.binding(function() { return list})
|
||||
|
|
|
@ -60,7 +60,8 @@ BaseDetailPage {
|
|||
}
|
||||
width: Constants.libraryDelegateWidth
|
||||
height: Constants.libraryDelegateHeight
|
||||
source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags.primary, "Primary", {"maxHeight": height})
|
||||
source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.jellyfinId, model.imageTags.Primary, "Primary", {"maxHeight": height})
|
||||
blurhash: model.imageBlurHashes.Primary[model.imageTags.Primary]
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
clip: true
|
||||
|
||||
|
@ -140,7 +141,7 @@ BaseDetailPage {
|
|||
wrapMode: Text.WordWrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.id})
|
||||
onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.jellyfinId})
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
|
|
|
@ -84,10 +84,10 @@ BaseDetailPage {
|
|||
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]
|
||||
poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.jellyfinId, 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})
|
||||
onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.jellyfinId})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ BaseDetailPage {
|
|||
property alias subtitle: pageHeader.description
|
||||
default property alias _data: content.data
|
||||
property real _playbackProsition: itemData.userData.playbackPositionTicks
|
||||
readonly property bool _userdataReady: itemData.status == JellyfinItem.Ready && itemData.userData != null
|
||||
SilicaFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: content.height + Theme.paddingLarge
|
||||
|
@ -57,8 +58,14 @@ BaseDetailPage {
|
|||
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
|
||||
Binding on favourited {
|
||||
when: _userdataReady
|
||||
value: itemData.userData.isFavorite
|
||||
}
|
||||
Binding on playProgress {
|
||||
when: _userdataReady
|
||||
value: itemData.userData.playedPercentage / 100
|
||||
}
|
||||
onPlayPressed: pageStack.push(Qt.resolvedUrl("../VideoPage.qml"),
|
||||
{"itemData": itemData,
|
||||
"audioTrack": trackSelector.audioTrack,
|
||||
|
|
|
@ -31,6 +31,7 @@ Dialog {
|
|||
id: loginDialog
|
||||
property string loginMessage
|
||||
property Page firstPage
|
||||
property User selectedUser: null
|
||||
|
||||
property string error
|
||||
|
||||
|
@ -92,16 +93,25 @@ Dialog {
|
|||
width: parent.width
|
||||
|
||||
Flow {
|
||||
id: userList
|
||||
width: parent.width
|
||||
Repeater {
|
||||
id: userRepeater
|
||||
model: userModel
|
||||
delegate: UserGridDelegate {
|
||||
name: model.name
|
||||
image: model.primaryImageTag ? "%1/Users/%2/Images/Primary?tag=%3".arg(ApiClient.baseUrl).arg(model.id).arg(model.primaryImageTag) : ""
|
||||
image: model.primaryImageTag ? "%1/Users/%2/Images/Primary?tag=%3".arg(ApiClient.baseUrl).arg(model.jellyfinId).arg(model.primaryImageTag) : ""
|
||||
highlighted: model.name === username.text
|
||||
onHighlightedChanged: {
|
||||
if (highlighted) {
|
||||
selectedUser = model.qtObject
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
username.text = model.name
|
||||
password.focus = true
|
||||
if (!password.activeFocus) {
|
||||
password.focus = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -119,18 +129,23 @@ Dialog {
|
|||
placeholderText: qsTr("Username")
|
||||
label: placeholderText
|
||||
errorHighlight: error
|
||||
EnterKey.iconSource: "image://theme/icon-m-enter-next"
|
||||
EnterKey.onClicked: password.focus = true
|
||||
EnterKey.iconSource: "image://theme/icon-m-enter-" + (password.enabled ? "next" : "accept")
|
||||
EnterKey.onClicked: password.enabled ? password.focus = true : accept()
|
||||
onTextChanged: {
|
||||
// Wil be executed before the onHighlightChanged of the UserDelegate
|
||||
// This is done to update the UI after an user is selected and this field has
|
||||
// been changed, to not let the UI be in an invalid state (e.g. no user selected, password field disabled)
|
||||
selectedUser = null
|
||||
}
|
||||
}
|
||||
|
||||
TextField {
|
||||
PasswordField {
|
||||
id: password
|
||||
width: parent.width
|
||||
|
||||
//: Label placeholder for password field
|
||||
placeholderText: qsTr("Password")
|
||||
label: placeholderText
|
||||
echoMode: TextInput.Password
|
||||
errorHighlight: error
|
||||
|
||||
EnterKey.iconSource: "image://theme/icon-m-enter-accept"
|
||||
|
@ -169,4 +184,41 @@ Dialog {
|
|||
}
|
||||
}
|
||||
canAccept: username.text
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "noUsers"
|
||||
when: userRepeater.count == 0
|
||||
PropertyChanges {
|
||||
target: userList
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "users"
|
||||
when: userRepeater.count != 0 && selectedUser === null
|
||||
PropertyChanges {
|
||||
target: userList
|
||||
visible: true
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "selectedUserPassword"
|
||||
when: selectedUser !== null && selectedUser.hasPassword
|
||||
extend: "users"
|
||||
PropertyChanges {
|
||||
target: password
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "selectedUserNoPassword"
|
||||
when: selectedUser !== null && !selectedUser.hasPassword
|
||||
extend: "users"
|
||||
PropertyChanges {
|
||||
target: password
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
#include <QtQuick>
|
||||
#endif
|
||||
|
||||
#include <QQmlDebuggingEnabler>
|
||||
#include <QCommandLineOption>
|
||||
#include <QCommandLineParser>
|
||||
#include <QDebug>
|
||||
|
@ -37,6 +38,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
static const char *SANDBOX_PROGRAM = "/usr/bin/sailjail";
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
QQmlDebuggingEnabler enabler;
|
||||
enabler.startTcpDebugServer(9999);
|
||||
// SailfishApp::main() will display "qml/harbour-sailfin.qml", if you need more
|
||||
// control over initialization, you can use:
|
||||
//
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue