1
0
Fork 0
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:
Chris Josten 2021-02-20 23:20:39 +01:00
parent 895731ae38
commit f7bca333c8
35 changed files with 1063 additions and 449 deletions

View file

@ -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
")

View file

@ -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)
}

View 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
}
}

View file

@ -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 {

View file

@ -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();
}
}

View file

@ -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 {} }

View file

@ -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)

View file

@ -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 {

View file

@ -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})
}
}
}

View file

@ -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:

View file

@ -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()
}

View file

@ -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 {

View file

@ -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})

View file

@ -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 {}

View file

@ -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})
}
}

View file

@ -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,

View file

@ -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
}
}
]
}

View file

@ -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:
//