1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2025-09-04 01:42:44 +00:00

Merge pull request #37 from heartfin/36-control-remote-jellyfin-sessions

Control remote jellyfin sessions
This commit is contained in:
Chris Josten 2024-01-02 15:24:00 +01:00 committed by GitHub
commit 3fb373a8ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
107 changed files with 8006 additions and 229 deletions

View file

@ -26,7 +26,7 @@ set(sailfin_QML_SOURCES
qml/components/videoplayer/VideoError.qml
qml/components/videoplayer/VideoHud.qml
qml/components/IconListItem.qml
qml/components/ItemChildrenShowcase.qml
qml/components/ItemChildrenShowcase.qml
qml/components/JItem.qml
qml/components/LibraryItemDelegate.qml
qml/components/MoreSection.qml
@ -39,14 +39,15 @@ set(sailfin_QML_SOURCES
qml/components/UserGridDelegate.qml
qml/components/VideoPlayer.qml
qml/components/VideoTrackSelector.qml
qml/cover/CollectionPage.qml
qml/cover/CollectionPage.qml
qml/cover/PosterCover.qml
qml/cover/NowPlayingCover.qml
qml/cover/NowPlayingCover.qml
qml/pages/LegalPage.qml
qml/pages/MainPage.qml
qml/pages/AboutPage.qml
qml/harbour-sailfin.qml
qml/pages/ConnectingPage.qml
qml/harbour-sailfin.qml
qml/pages/ConnectingPage.qml
qml/pages/ControllableDevicesPage.qml
qml/pages/SettingsPage.qml
qml/pages/VideoPage.qml
qml/pages/itemdetails/BaseDetailPage.qml
@ -54,8 +55,8 @@ set(sailfin_QML_SOURCES
qml/pages/itemdetails/EpisodePage.qml
qml/pages/itemdetails/FilmPage.qml
qml/pages/itemdetails/MusicAlbumPage.qml
qml/pages/itemdetails/MusicArtistPage.qml
qml/pages/itemdetails/MusicLibraryPage.qml
qml/pages/itemdetails/MusicArtistPage.qml
qml/pages/itemdetails/MusicLibraryPage.qml
qml/pages/itemdetails/PhotoPage.qml
qml/pages/itemdetails/SeasonPage.qml
qml/pages/itemdetails/SeriesPage.qml

View file

@ -69,7 +69,11 @@ function randomBackdrop(baseUrl, item) {
function itemBackdropUrl(baseUrl, item, idx, options) {
var extraQuery = propsToQuery(options)
return baseUrl + "/Items/" + item.jellyfinId + "/Images/Backdrop/" + idx + "?tag=" + item.backdropImageTags[idx] + extraQuery;
if (item.backdropImageTags[idx]) {
return baseUrl + "/Items/" + item.jellyfinId + "/Images/Backdrop/" + idx + "?tag=" + item.backdropImageTags[idx] + extraQuery;
} else {
return baseUrl + "/Items/" + item.parentBackdropItemId + "/Images/Backdrop/" + idx + "?tag=" + item.parentBackdropImageTags[idx] + extraQuery;
}
}

View file

@ -48,6 +48,8 @@ PanelBackground {
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
@ -67,9 +69,12 @@ PanelBackground {
top: parent.top
}
width: height
Binding on blurhash {
when: manager.item !== null && "Primary" in manager.item.imageBlurHashes && "Primary" in manager.item.imageTags
value: manager.item.imageBlurHashes["Primary"][manager.item.imageTags["Primary"]]
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
@ -125,7 +130,10 @@ PanelBackground {
Label {
id: name
text: manager.item === null ? qsTr("No media selected") : manager.item.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
@ -133,8 +141,23 @@ PanelBackground {
}
Label {
id: artists
leftPadding: controllingRemote ? remoteIcon.width + Theme.paddingSmall : 0
text: {
if (manager.item === null) return qsTr("Play some media!")
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 = [];
@ -147,7 +170,7 @@ PanelBackground {
.arg(Theme.secondaryColor)
)
}
return links.join(", ")
return remoteText + links.join(", ")
}
return qsTr("No audio")
}
@ -162,6 +185,16 @@ PanelBackground {
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
}
}
}
@ -267,7 +300,16 @@ PanelBackground {
states: [
State {
name: ""
when: manager.playbackState !== J.PlayerState.Stopped && !isFullPage && !("__hidePlaybackBar" in pageStack.currentPage)
// 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"
@ -386,7 +428,9 @@ PanelBackground {
},
State {
name: "hidden"
when: ((manager.playbackState === J.PlayerState.Stopped && !mediaLoading) || "__hidePlaybackBar" in pageStack.currentPage) && !isFullPage
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

View file

@ -50,6 +50,16 @@ SilicaItem {
color: Theme.overlayBackgroundColor
}
RemoteImage {
id: backdrop
anchors.fill: parent
visible: !manager.controllingSessionLocal
|| [J.MediaStatus.NoMedia, J.MediaStatus.Loading].indexOf(manager.mediaStatus) >= 0
fillMode: Image.PreserveAspectFit
source: Utils.itemBackdropUrl(apiClient.baseUrl, item, 0, {"maxWidth": parent.width})
blurhash: item.imageBlurHashes["Backdrop"][item.backdropImageTags[0]]
}
VideoOutput {
id: videoOutput
source: manager
@ -64,7 +74,7 @@ SilicaItem {
anchors.fill: parent
manager: playerRoot.manager
title: videoPlayer.title
alwaysVisible: !manager.controllingSessionLocal
}
VideoError {

View file

@ -34,6 +34,8 @@ Item {
property var manager
property string title
property bool _manuallyActivated: false
/// Don't allow the HUD to hide
property bool alwaysVisible: false
readonly property bool hidden: opacity == 0.0
Behavior on opacity { FadeAnimator {} }
@ -174,6 +176,7 @@ Item {
}
function hide(manual) {
if (alwaysVisible) return
// Don't hide if the user decided on their own to show the hud
//if (!manual && _manuallyActivated) return;
// Don't give in to the user if they want to hide the hud while it was forced upon them

View file

@ -50,6 +50,8 @@ ApplicationWindow {
ApiClient {
id: _apiClient
objectName: "Test"
appName: "Sailfin"
deviceType: DeviceType.Phone
supportedCommands: [GeneralCommandType.Play, GeneralCommandType.DisplayMessage]
}
@ -74,7 +76,7 @@ ApplicationWindow {
//cover: CoverBackground {CoverPlaceholder { icon.source: "icon.png"; text: "Sailfin"}}
cover: {
// Disabled due to buggy Loader behaviour
if ([MediaPlayer.NoMedia, MediaPlayer.InvalidMedia, MediaPlayer.UnknownStatus].indexOf(_playbackManager.mediaStatus) >= 0
if ([MediaStatus.NoMedia, MediaStatus.InvalidMedia].indexOf(_playbackManager.mediaStatus) >= 0
|| _playbackManager.playbackState === MediaPlayer.StoppedState) {
return Qt.resolvedUrl("cover/CollectionPage.qml")
} else {
@ -133,7 +135,9 @@ ApplicationWindow {
}
DisplayBlanking {
preventBlanking: playbackManager.playbackState === MediaPlayer.PlayingState && playbackManager.hasVideo
preventBlanking: playbackManager.playbackState === MediaPlayer.PlayingState
&& playbackManager.hasVideo
&& playbackManager.controllingSessionLocal // Must be controlling a local session
}
PlaybackBar {

View file

@ -0,0 +1,75 @@
import QtQuick 2.6
import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J
import ".."
Page {
id: pageRoot
SilicaListView {
id: listView
anchors.fill: parent
contentHeight: Theme.itemSizeLarge
header: PageHeader {
//: Page title: page for remote controlling other Jellyfin apps
title: qsTr("Remote control")
}
model: J.RemoteDeviceList {
id: deviceList
apiClient: appWindow.apiClient
scanning: pageRoot.status == PageStatus.Active
}
delegate: ListItem {
property bool isConnected: model.jellyfinId === appWindow.playbackManager.controllingSessionId
onClicked: deviceList.activateSession(appWindow.playbackManager, model.index)
contentHeight: Theme.itemSizeMedium
Icon {
id: deviceIcon
anchors {
left: parent.left
leftMargin: Theme.horizontalPageMargin
verticalCenter: parent.verticalCenter
}
height: parent.contentHeight - 2 * Theme.paddingMedium
width: height
source: {
if (model.deviceType == J.DeviceType.Phone) {
return "image://theme/icon-m-device"
} else if (model.deviceType == J.DeviceType.Tv) {
// For the lack of a better icon
return "image://theme/icon-m-device-landscape"
} else if (model.deviceType == J.DeviceType.Computer) {
return "image://theme/icon-m-computer"
} else {
//case J.DeviceType.Unknown:
return "image://theme/icon-m-question"
}
}
highlighted: parent.down || isConnected
}
Column {
anchors {
left: deviceIcon.right
right: parent.right
verticalCenter: parent.verticalCenter
leftMargin: Theme.paddingLarge
rightMargin: Theme.horizontalPageMargin
}
Label {
id: deviceName
//: List of devices item title in the form of <app name> <device name>
text: qsTr("%1 — %2").arg(model.name).arg(model.deviceName)
color: isConnected || highlighted ? Theme.highlightColor : Theme.primaryColor
}
Label {
id: deviceUser
text: model.userName
color: isConnected || highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor
}
}
}
}
}

View file

@ -45,6 +45,11 @@ Page {
text: qsTr("Settings")
onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
}
MenuItem {
//: Pulley menu item: shows controllable device page
text: qsTr("Remote control")
onClicked: pageStack.push(Qt.resolvedUrl("ControllableDevicesPage.qml"))
}
MenuItem {
//: Pulley menu item: reload items on page
text: qsTr("Reload")

View file

@ -38,7 +38,16 @@ Page {
property int subtitleTrack
property bool resume: true
allowedOrientations: Orientation.All
allowedOrientations: {
if (itemData.width !== null && itemData.height !== null) {
return itemData.width / itemData.height > Screen.width / Screen.height
? Orientation.LandscapeMask
: Orientation.PortraitMask
} else {
return Orientation.All
}
}
showNavigationIndicator: videoPlayer.hudVisible
VideoPlayer {