diff --git a/CMakeLists.txt b/CMakeLists.txt
index 25a2be4..1f4966d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -9,6 +9,7 @@ set(CMAKE_CXX_STANDARD 17)
# Options
option(PLATFORM_SAILFISHOS "Build SailfishOS version of application" OFF)
option(PLATFORM_QTQUICK "Build QtQuick version of application" ON)
+option(FREEDESKTOP_INTEGRATION "Integration with various FreeDesktop.org standards, such as MPRIS" ON)
option(BUILD_PRECOMPILED_HEADERS "Build with precompiled headers for faster compile times when doing a full rebuild, at the cost of slower incremental builds whenever a header file is changed" OFF)
if (NOT SAILFIN_VERSION)
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
index a45f0d1..41316fc 100644
--- a/core/CMakeLists.txt
+++ b/core/CMakeLists.txt
@@ -1,5 +1,10 @@
project(jellyfin-qt VERSION 0.1.0)
find_package(Qt5 5.6 COMPONENTS Multimedia Network Qml WebSockets REQUIRED)
+
+if (FREEDESKTOP_INTEGRATION)
+find_package(Qt5 5.6 COMPONENTS DBus REQUIRED)
+endif()
+
include(GNUInstallDirs)
include(GeneratedSources.cmake)
@@ -52,6 +57,7 @@ set(JellyfinQt_HEADERS
include/JellyfinQt/viewmodel/modelstatus.h
include/JellyfinQt/viewmodel/propertyhelper.h
include/JellyfinQt/viewmodel/playbackmanager.h
+ include/JellyfinQt/viewmodel/platformmediacontrol.h
include/JellyfinQt/viewmodel/playlist.h
include/JellyfinQt/viewmodel/userdata.h
include/JellyfinQt/viewmodel/usermodel.h
@@ -64,6 +70,19 @@ set(JellyfinQt_HEADERS
include/JellyfinQt/jsonhelper.h
include/JellyfinQt/serverdiscoverymodel.h
include/JellyfinQt/websocket.h)
+
+if (FREEDESKTOP_INTEGRATION)
+ list(APPEND JellyfinQt_SOURCES
+ src/platform/freedesktop/mediaplayer2.cpp
+ src/platform/freedesktop/mediaplayer2player.cpp
+ src/viewmodel/platformmediacontrol_freedesktop.cpp)
+ list(APPEND JellyfinQt_HEADERS
+ include/JellyfinQt/platform/freedesktop/mediaplayer2.h
+ include/JellyfinQt/platform/freedesktop/mediaplayer2player.h)
+else()
+ list(APPEND JellyfinQt_SOURCES
+ src/viewmodel/platformmediacontrol_stub.cpp)
+endif()
list(APPEND JellyfinQt_HEADERS ${openapi_HEADERS})
@@ -83,6 +102,10 @@ endif()
target_include_directories(JellyfinQt PUBLIC "include")
target_link_libraries(JellyfinQt PUBLIC Qt5::Core Qt5::Multimedia Qt5::Network Qt5::Qml Qt5::WebSockets)
+
+if (FREEDESKTOP_INTEGRATION)
+ target_link_libraries(JellyfinQt PUBLIC Qt5::DBus)
+endif()
set_target_properties(JellyfinQt PROPERTIES CXX_VISIBILITY_PRESET default)
install(TARGETS JellyfinQt
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
diff --git a/core/dbus/org.mpris.MediaPlayer2.Player.xml b/core/dbus/org.mpris.MediaPlayer2.Player.xml
new file mode 100644
index 0000000..9cfc223
--- /dev/null
+++ b/core/dbus/org.mpris.MediaPlayer2.Player.xml
@@ -0,0 +1,673 @@
+
+
+
+
+
+
+ This interface implements the methods for querying and providing basic
+ control over what is currently playing.
+
+
+
+
+
+
+ A track is currently playing.
+
+
+
+
+ A track is currently paused.
+
+
+
+
+ There is no track currently playing.
+
+
+
+ A playback state.
+
+
+
+
+
+
+ The playback will stop when there are no more tracks to play
+
+
+
+
+ The current track will start again from the begining once it has finished playing
+
+
+
+
+ The playback loops through a list of tracks
+
+
+
+ A repeat / loop status
+
+
+
+
+
+ Unique track identifier.
+
+ If the media player implements the TrackList interface and allows
+ the same track to appear multiple times in the tracklist,
+ this must be unique within the scope of the tracklist.
+
+
+ Note that this should be a valid D-Bus object id, although clients
+ should not assume that any object is actually exported with any
+ interfaces at that path.
+
+
+ Media players may not use any paths starting with
+ /org/mpris unless explicitly allowed by this specification.
+ Such paths are intended to have special meaning, such as
+ /org/mpris/MediaPlayer2/TrackList/NoTrack
+ to indicate "no track".
+
+
+
+ This is a D-Bus object id as that is the definitive way to have
+ unique identifiers on D-Bus. It also allows for future optional
+ expansions to the specification where tracks are exported to D-Bus
+ with an interface similar to org.gnome.UPnP.MediaItem2.
+
+
+
+
+
+
+
+ A playback rate
+
+ This is a multiplier, so a value of 0.5 indicates that playback is
+ happening at half speed, while 1.5 means that 1.5 seconds of "track time"
+ is consumed every second.
+
+
+
+
+
+
+ Audio volume level
+
+ - 0.0 means mute.
+ - 1.0 is a sensible maximum volume level (ex: 0dB).
+
+
+ Note that the volume may be higher than 1.0, although generally
+ clients should not attempt to set it above 1.0.
+
+
+
+
+
+
+ Time in microseconds.
+
+
+
+
+
+ Skips to the next track in the tracklist.
+
+ If there is no next track (and endless playback and track
+ repeat are both off), stop playback.
+
+ If playback is paused or stopped, it remains that way.
+
+ If CanGoNext is
+ false, attempting to call this method should have
+ no effect.
+
+
+
+
+
+
+ Skips to the previous track in the tracklist.
+
+ If there is no previous track (and endless playback and track
+ repeat are both off), stop playback.
+
+ If playback is paused or stopped, it remains that way.
+
+ If CanGoPrevious is
+ false, attempting to call this method should have
+ no effect.
+
+
+
+
+
+
+ Pauses playback.
+ If playback is already paused, this has no effect.
+
+ Calling Play after this should cause playback to start again
+ from the same position.
+
+
+ If CanPause is
+ false, attempting to call this method should have
+ no effect.
+
+
+
+
+
+
+ Pauses playback.
+ If playback is already paused, resumes playback.
+ If playback is stopped, starts playback.
+
+ If CanPause is
+ false, attempting to call this method should have
+ no effect and raise an error.
+
+
+
+
+
+
+ Stops playback.
+ If playback is already stopped, this has no effect.
+
+ Calling Play after this should cause playback to
+ start again from the beginning of the track.
+
+
+ If CanControl is
+ false, attempting to call this method should have
+ no effect and raise an error.
+
+
+
+
+
+
+ Starts or resumes playback.
+ If already playing, this has no effect.
+ If paused, playback resumes from the current position.
+ If there is no track to play, this has no effect.
+
+ If CanPlay is
+ false, attempting to call this method should have
+ no effect.
+
+
+
+
+
+
+
+ The number of microseconds to seek forward.
+
+
+
+
+ Seeks forward in the current track by the specified number
+ of microseconds.
+
+
+ A negative value seeks back. If this would mean seeking
+ back further than the start of the track, the position
+ is set to 0.
+
+
+ If the value passed in would mean seeking beyond the end
+ of the track, acts like a call to Next.
+
+
+ If the CanSeek property is false,
+ this has no effect.
+
+
+
+
+
+
+
+ The currently playing track's identifier.
+
+ If this does not match the id of the currently-playing track,
+ the call is ignored as "stale".
+
+
+ /org/mpris/MediaPlayer2/TrackList/NoTrack
+ is not a valid value for this argument.
+
+
+
+
+
+ Track position in microseconds.
+ This must be between 0 and <track_length>.
+
+
+
+ Sets the current track position in microseconds.
+ If the Position argument is less than 0, do nothing.
+
+ If the Position argument is greater than the track length,
+ do nothing.
+
+
+ If the CanSeek property is false,
+ this has no effect.
+
+
+
+ The reason for having this method, rather than making
+ Position writable, is to include
+ the TrackId argument to avoid race conditions where a client tries
+ to seek to a position when the track has already changed.
+
+
+
+
+
+
+
+
+
+ Uri of the track to load. Its uri scheme should be an element of the
+ org.mpris.MediaPlayer2.SupportedUriSchemes
+ property and the mime-type should match one of the elements of the
+ org.mpris.MediaPlayer2.SupportedMimeTypes.
+
+
+
+
+ Opens the Uri given as an argument
+ If the playback is stopped, starts playing
+
+ If the uri scheme or the mime-type of the uri to open is not supported,
+ this method does nothing and may raise an error. In particular, if the
+ list of available uri schemes is empty, this method may not be
+ implemented.
+
+ Clients should not assume that the Uri has been opened as soon as this
+ method returns. They should wait until the mpris:trackid field in the
+ Metadata property changes.
+
+
+ If the media player implements the TrackList interface, then the
+ opened track should be made part of the tracklist, the
+ org.mpris.MediaPlayer2.TrackList.TrackAdded or
+ org.mpris.MediaPlayer2.TrackList.TrackListReplaced
+ signal should be fired, as well as the
+ org.freedesktop.DBus.Properties.PropertiesChanged
+ signal on the tracklist interface.
+
+
+
+
+
+
+
+ The current playback status.
+
+ May be "Playing", "Paused" or "Stopped".
+
+
+
+
+
+
+
+
+ The current loop / repeat status
+ May be:
+
+ - "None" if the playback will stop when there are no more tracks to play
+ - "Track" if the current track will start again from the begining once it has finished playing
+ - "Playlist" if the playback loops through a list of tracks
+
+
+
+ If CanControl is
+ false, attempting to set this property should have
+ no effect and raise an error.
+
+
+
+
+
+
+
+ The current playback rate.
+
+ The value must fall in the range described by
+ MinimumRate and
+ MaximumRate, and must not be 0.0. If
+ playback is paused, the PlaybackStatus
+ property should be used to indicate this. A value of 0.0 should not
+ be set by the client. If it is, the media player should act as
+ though Pause was called.
+
+
+ If the media player has no ability to play at speeds other than the
+ normal playback rate, this must still be implemented, and must
+ return 1.0. The MinimumRate and
+ MaximumRate properties must also be
+ set to 1.0.
+
+
+ Not all values may be accepted by the media player. It is left to
+ media player implementations to decide how to deal with values they
+ cannot use; they may either ignore them or pick a "best fit" value.
+ Clients are recommended to only use sensible fractions or multiples
+ of 1 (eg: 0.5, 0.25, 1.5, 2.0, etc).
+
+
+
+ This allows clients to display (reasonably) accurate progress bars
+ without having to regularly query the media player for the current
+ position.
+
+
+
+
+
+
+
+
+
+
+ A value of false indicates that playback is
+ progressing linearly through a playlist, while true
+ means playback is progressing through a playlist in some other order.
+
+
+ If CanControl is
+ false, attempting to set this property should have
+ no effect and raise an error.
+
+
+
+
+
+
+
+
+ The metadata of the current element.
+
+ If there is a current track, this must have a "mpris:trackid" entry
+ (of D-Bus type "o") at the very least, which contains a D-Bus path that
+ uniquely identifies this track.
+
+
+ See the type documentation for more details.
+
+
+
+
+
+
+
+ The volume level.
+
+ When setting, if a negative value is passed, the volume
+ should be set to 0.0.
+
+
+ If CanControl is
+ false, attempting to set this property should have
+ no effect and raise an error.
+
+
+
+
+
+
+
+
+ The current track position in microseconds, between 0 and
+ the 'mpris:length' metadata entry (see Metadata).
+
+
+ Note: If the media player allows it, the current playback position
+ can be changed either the SetPosition method or the Seek method on
+ this interface. If this is not the case, the
+ CanSeek property is false, and
+ setting this property has no effect and can raise an error.
+
+
+ If the playback progresses in a way that is inconstistant with the
+ Rate property, the
+ Seeked signal is emited.
+
+
+
+
+
+
+
+
+ The minimum value which the Rate
+ property can take.
+ Clients should not attempt to set the
+ Rate property below this value.
+
+
+ Note that even if this value is 0.0 or negative, clients should
+ not attempt to set the Rate property
+ to 0.0.
+
+ This value should always be 1.0 or less.
+
+
+
+
+
+
+
+ The maximum value which the Rate
+ property can take.
+ Clients should not attempt to set the
+ Rate property above this value.
+
+
+ This value should always be 1.0 or greater.
+
+
+
+
+
+
+
+
+ Whether the client can call the Next
+ method on this interface and expect the current track to change.
+
+
+ If it is unknown whether a call to Next will
+ be successful (for example, when streaming tracks), this property should
+ be set to true.
+
+
+ If CanControl is
+ false, this property should also be
+ false.
+
+
+
+ Even when playback can generally be controlled, there may not
+ always be a next track to move to.
+
+
+
+
+
+
+
+
+
+ Whether the client can call the
+ Previous method on this interface and
+ expect the current track to change.
+
+
+ If it is unknown whether a call to Previous
+ will be successful (for example, when streaming tracks), this property
+ should be set to true.
+
+
+ If CanControl is
+ false, this property should also be
+ false.
+
+
+
+ Even when playback can generally be controlled, there may not
+ always be a next previous to move to.
+
+
+
+
+
+
+
+
+
+ Whether playback can be started using
+ Play or
+ PlayPause.
+
+
+ Note that this is related to whether there is a "current track": the
+ value should not depend on whether the track is currently paused or
+ playing. In fact, if a track is currently playing (and
+ CanControl is true),
+ this should be true.
+
+
+ If CanControl is
+ false, this property should also be
+ false.
+
+
+
+ Even when playback can generally be controlled, it may not be
+ possible to enter a "playing" state, for example if there is no
+ "current track".
+
+
+
+
+
+
+
+
+ Whether playback can be paused using
+ Pause or
+ PlayPause.
+
+
+ Note that this is an intrinsic property of the current track: its
+ value should not depend on whether the track is currently paused or
+ playing. In fact, if playback is currently paused (and
+ CanControl is true),
+ this should be true.
+
+
+ If CanControl is
+ false, this property should also be
+ false.
+
+
+
+ Not all media is pausable: it may not be possible to pause some
+ streamed media, for example.
+
+
+
+
+
+
+
+
+
+ Whether the client can control the playback position using
+ Seek and
+ SetPosition. This may be different for
+ different tracks.
+
+
+ If CanControl is
+ false, this property should also be
+ false.
+
+
+
+ Not all media is seekable: it may not be possible to seek when
+ playing some streamed media, for example.
+
+
+
+
+
+
+
+
+ Whether the media player may be controlled over this interface.
+
+ This property is not expected to change, as it describes an intrinsic
+ capability of the implementation.
+
+
+ If this is false, clients should assume that all
+ properties on this interface are read-only (and will raise errors
+ if writing to them is attempted), no methods are implemented
+ and all other properties starting with "Can" are also
+ false.
+
+
+
+ This allows clients to determine whether to present and enable
+ controls to the user in advance of attempting to call methods
+ and write to properties.
+
+
+
+
+
+
+
+
+ The new position, in microseconds.
+
+
+
+
+ Indicates that the track position has changed in a way that is
+ inconsistant with the current playing state.
+
+ When this signal is not received, clients should assume that:
+
+ -
+ When playing, the position progresses according to the rate property.
+
+ - When paused, it remains constant.
+
+
+ This signal does not need to be emitted when playback starts
+ or when the track changes, unless the track is starting at an
+ unexpected position. An expected position would be the last
+ known one when going from Paused to Playing, and 0 when going from
+ Stopped to Playing.
+
+
+
+
+
+
+
diff --git a/core/dbus/org.mpris.MediaPlayer2.Playlists.xml b/core/dbus/org.mpris.MediaPlayer2.Playlists.xml
new file mode 100644
index 0000000..e285ef2
--- /dev/null
+++ b/core/dbus/org.mpris.MediaPlayer2.Playlists.xml
@@ -0,0 +1,250 @@
+
+
+
+
+
+ Provides access to the media player's playlists.
+
+ Since D-Bus does not provide an easy way to check for what interfaces
+ are exported on an object, clients should attempt to get one of the
+ properties on this interface to see if it is implemented.
+
+
+
+
+
+ Unique playlist identifier.
+
+
+ Multiple playlists may have the same name.
+
+
+ This is a D-Bus object id as that is the definitive way to have
+ unique identifiers on D-Bus. It also allows for future optional
+ expansions to the specification where tracks are exported to D-Bus
+ with an interface similar to org.gnome.UPnP.MediaItem2.
+
+
+
+
+
+
+
+ A URI.
+
+
+
+
+
+ A data structure describing a playlist.
+
+
+
+ A unique identifier for the playlist.
+ This should remain the same if the playlist is renamed.
+
+
+
+
+ The name of the playlist, typically given by the user.
+
+
+
+
+ The URI of an (optional) icon.
+
+
+
+
+
+
+ A data structure describing a playlist, or nothing.
+
+
+ D-Bus does not (at the time of writing) support a MAYBE type,
+ so we are forced to invent our own.
+
+
+
+
+
+ Whether this structure refers to a valid playlist.
+
+
+
+
+ The playlist, providing Valid is true, otherwise undefined.
+
+ When constructing this type, it should be noted that the playlist
+ ID must be a valid object path, or D-Bus implementations may reject
+ it. This is true even when Valid is false. It is suggested that
+ "/" is used as the playlist ID in this case.
+
+
+
+
+
+
+
+ Specifies the ordering of returned playlists.
+
+
+
+ Alphabetical ordering by name, ascending.
+
+
+
+
+ Ordering by creation date, oldest first.
+
+
+
+
+ Ordering by last modified date, oldest first.
+
+
+
+
+ Ordering by date of last playback, oldest first.
+
+
+
+
+ A user-defined ordering.
+
+
+ Some media players may allow users to order playlists as they
+ wish. This ordering allows playlists to be retreived in that
+ order.
+
+
+
+
+
+
+
+
+
+ Starts playing the given playlist.
+
+
+ Note that this must be implemented. If the media player does not
+ allow clients to change the playlist, it should not implement this
+ interface at all.
+
+
+ It is up to the media player whether this completely replaces the
+ current tracklist, or whether it is merely inserted into the
+ tracklist and the first track starts. For example, if the media
+ player is operating in a "jukebox" mode, it may just append the
+ playlist to the list of upcoming tracks, and skip to the first
+ track in the playlist.
+
+
+
+
+ The id of the playlist to activate.
+
+
+
+
+
+
+ Gets a set of playlists.
+
+
+
+ The index of the first playlist to be fetched (according to the ordering).
+
+
+
+
+ The maximum number of playlists to fetch.
+
+
+
+
+ The ordering that should be used.
+
+
+
+
+ Whether the order should be reversed.
+
+
+
+
+ A list of (at most MaxCount) playlists.
+
+
+
+
+
+
+
+
+ The number of playlists available.
+
+
+
+
+
+
+
+
+ The available orderings. At least one must be offered.
+
+
+
+ Media players may not have access to all the data required for some
+ orderings. For example, creation times are not available on UNIX
+ filesystems (don't let the ctime fool you!). On the other hand,
+ clients should have some way to get the "most recent" playlists.
+
+
+
+
+
+
+
+
+
+ The currently-active playlist.
+
+
+ If there is no currently-active playlist, the structure's Valid field
+ will be false, and the Playlist details are undefined.
+
+
+ Note that this may not have a value even after ActivatePlaylist is
+ called with a valid playlist id as ActivatePlaylist implementations
+ have the option of simply inserting the contents of the playlist into
+ the current tracklist.
+
+
+
+
+
+
+
+ The playlist which details have changed.
+
+
+
+ Indicates that either the Name or Icon attribute of a
+ playlist has changed.
+
+ Client implementations should be aware that this signal
+ may not be implemented.
+
+
+ Without this signal, media players have no way to notify clients
+ of a change in the attributes of a playlist other than the active one
+
+
+
+
+
+
+
+
diff --git a/core/dbus/org.mpris.MediaPlayer2.TrackList.xml b/core/dbus/org.mpris.MediaPlayer2.TrackList.xml
new file mode 100644
index 0000000..6c511df
--- /dev/null
+++ b/core/dbus/org.mpris.MediaPlayer2.TrackList.xml
@@ -0,0 +1,349 @@
+
+
+
+
+
+
+ Provides access to a short list of tracks which were recently played or
+ will be played shortly. This is intended to provide context to the
+ currently-playing track, rather than giving complete access to the
+ media player's playlist.
+
+
+ Example use cases are the list of tracks from the same album as the
+ currently playing song or the
+ Rhythmbox play queue.
+
+
+ Each track in the tracklist has a unique identifier.
+ The intention is that this uniquely identifies the track within
+ the scope of the tracklist. In particular, if a media item
+ (a particular music file, say) occurs twice in the track list, each
+ occurrence should have a different identifier. If a track is removed
+ from the middle of the playlist, it should not affect the track ids
+ of any other tracks in the tracklist.
+
+
+ As a result, the traditional track identifiers of URLs and position
+ in the playlist cannot be used. Any scheme which satisfies the
+ uniqueness requirements is valid, as clients should not make any
+ assumptions about the value of the track id beyond the fact
+ that it is a unique identifier.
+
+
+ Note that the (memory and processing) burden of implementing the
+ TrackList interface and maintaining unique track ids for the
+ playlist can be mitigated by only exposing a subset of the playlist when
+ it is very long (the 20 or so tracks around the currently playing
+ track, for example). This is a recommended practice as the tracklist
+ interface is not designed to enable browsing through a large list of tracks,
+ but rather to provide clients with context about the currently playing track.
+
+
+
+
+
+ A mapping from metadata attribute names to values.
+
+ The mpris:trackid attribute must always be present, and must be
+ of D-Bus type "o". This contains a D-Bus path that uniquely identifies
+ the track within the scope of the playlist. There may or may not be
+ an actual D-Bus object at that path; this specification says nothing
+ about what interfaces such an object may implement.
+
+
+ If the length of the track is known, it should be provided in the
+ metadata property with the "mpris:length" key. The length must be
+ given in microseconds, and be represented as a signed 64-bit integer.
+
+
+ If there is an image associated with the track, a URL for it may be
+ provided using the "mpris:artUrl" key. For other metadata, fields
+ defined by the
+ Xesam ontology
+ should be used, prefixed by "xesam:". See the
+ metadata page on the freedesktop.org wiki
+ for a list of common fields.
+
+
+ Lists of strings should be passed using the array-of-string ("as")
+ D-Bus type. Dates should be passed as strings using the ISO 8601
+ extended format (eg: 2007-04-29T14:35:51). If the timezone is
+ known, RFC 3339's internet profile should be used (eg:
+ 2007-04-29T14:35:51+02:00).
+
+
+
+
+
+ The name of the attribute; see the
+ metadata page
+ for guidelines on names to use.
+
+
+
+
+
+ The value of the attribute, in the most appropriate format.
+
+
+
+
+
+
+ A unique resource identifier.
+
+
+
+
+
+
+ The list of track ids for which metadata is requested.
+
+
+
+
+ Metadata of the set of tracks given as input.
+ See the type documentation for more details.
+
+
+
+ Gets all the metadata available for a set of tracks.
+
+ Each set of metadata must have a "mpris:trackid" entry at the very least,
+ which contains a string that uniquely identifies this track within
+ the scope of the tracklist.
+
+
+
+
+
+
+
+
+ The uri of the item to add. Its uri scheme should be an element of the
+ org.mpris.MediaPlayer2.SupportedUriSchemes
+ property and the mime-type should match one of the elements of the
+ org.mpris.MediaPlayer2.SupportedMimeTypes
+
+
+
+
+
+
+ The identifier of the track after which
+ the new item should be inserted. The path
+ /org/mpris/MediaPlayer2/TrackList/NoTrack
+ indicates that the track should be inserted at the
+ start of the track list.
+
+
+
+
+
+
+ Whether the newly inserted track should be considered as
+ the current track. Setting this to true has the same effect as
+ calling GoTo afterwards.
+
+
+
+
+ Adds a URI in the TrackList.
+
+ If the CanEditTracks property is false,
+ this has no effect.
+
+
+ Note: Clients should not assume that the track has been added at the
+ time when this method returns. They should wait for a TrackAdded (or
+ TrackListReplaced) signal.
+
+
+
+
+
+
+
+ Identifier of the track to be removed.
+
+ /org/mpris/MediaPlayer2/TrackList/NoTrack
+ is not a valid value for this argument.
+
+
+
+
+ Removes an item from the TrackList.
+ If the track is not part of this tracklist, this has no effect.
+
+ If the CanEditTracks property is false,
+ this has no effect.
+
+
+ Note: Clients should not assume that the track has been removed at the
+ time when this method returns. They should wait for a TrackRemoved (or
+ TrackListReplaced) signal.
+
+
+
+
+
+
+
+ Identifier of the track to skip to.
+
+ /org/mpris/MediaPlayer2/TrackList/NoTrack
+ is not a valid value for this argument.
+
+
+
+
+ Skip to the specified TrackId.
+ If the track is not part of this tracklist, this has no effect.
+
+ If this object is not /org/mpris/MediaPlayer2,
+ the current TrackList's tracks should be replaced with the contents of
+ this TrackList, and the TrackListReplaced signal should be fired from
+ /org/mpris/MediaPlayer2.
+
+
+
+
+
+
+
+
+ An array which contains the identifier of each track
+ in the tracklist, in order.
+
+
+ The org.freedesktop.DBus.Properties.PropertiesChanged
+ signal is emited every time this property changes, but the signal
+ message does not contain the new value.
+
+ Client implementations should rather rely on the
+ TrackAdded,
+ TrackRemoved and
+ TrackListReplaced signals to keep their
+ representation of the tracklist up to date.
+
+
+
+
+
+
+
+
+ If false, calling
+ AddTrack or
+ RemoveTrack will have no effect,
+ and may raise a NotSupported error.
+
+
+
+
+
+
+
+ The new content of the tracklist.
+
+
+
+
+ The identifier of the track to be considered as current.
+
+ /org/mpris/MediaPlayer2/TrackList/NoTrack
+ indicates that there is no current track.
+
+
+ This should correspond to the mpris:trackid field of the
+ Metadata property of the org.mpris.MediaPlayer2.Player
+ interface.
+
+
+
+
+ Indicates that the entire tracklist has been replaced.
+
+ It is left up to the implementation to decide when
+ a change to the track list is invasive enough that
+ this signal should be emitted instead of a series of
+ TrackAdded and TrackRemoved signals.
+
+
+
+
+
+
+
+ The metadata of the newly added item.
+ This must include a mpris:trackid entry.
+ See the type documentation for more details.
+
+
+
+
+
+ The identifier of the track after which the new track
+ was inserted. The path
+ /org/mpris/MediaPlayer2/TrackList/NoTrack
+ indicates that the track was inserted at the
+ start of the track list.
+
+
+
+
+ Indicates that a track has been added to the track list.
+
+
+
+
+
+
+ The identifier of the track being removed.
+
+ /org/mpris/MediaPlayer2/TrackList/NoTrack
+ is not a valid value for this argument.
+
+
+
+
+ Indicates that a track has been removed from the track list.
+
+
+
+
+
+
+ The id of the track which metadata has changed.
+ If the track id has changed, this will be the old value.
+
+ /org/mpris/MediaPlayer2/TrackList/NoTrack
+ is not a valid value for this argument.
+
+
+
+
+
+ The new track metadata.
+
+ This must include a mpris:trackid entry. If the track id has
+ changed, this will be the new value.
+
+ See the type documentation for more details.
+
+
+
+
+ Indicates that the metadata of a track in the tracklist has changed.
+
+
+ This may indicate that a track has been replaced, in which case the
+ mpris:trackid metadata entry is different from the TrackId argument.
+
+
+
+
+
+
+
diff --git a/core/dbus/org.mpris.MediaPlayer2.xml b/core/dbus/org.mpris.MediaPlayer2.xml
new file mode 100644
index 0000000..aca33e6
--- /dev/null
+++ b/core/dbus/org.mpris.MediaPlayer2.xml
@@ -0,0 +1,198 @@
+
+
+
+
+
+
+
+
+ Brings the media player's user interface to the front using any
+ appropriate mechanism available.
+
+
+ The media player may be unable to control how its user interface
+ is displayed, or it may not have a graphical user interface at all.
+ In this case, the CanRaise property is
+ false and this method does nothing.
+
+
+
+
+
+
+ Causes the media player to stop running.
+
+ The media player may refuse to allow clients to shut it down.
+ In this case, the CanQuit property is
+ false and this method does nothing.
+
+
+ Note: Media players which can be D-Bus activated, or for which there is
+ no sensibly easy way to terminate a running instance (via the main
+ interface or a notification area icon for example) should allow clients
+ to use this method. Otherwise, it should not be needed.
+
+ If the media player does not have a UI, this should be implemented.
+
+
+
+
+
+
+ If false, calling
+ Quit will have no effect, and may
+ raise a NotSupported error. If true, calling
+ Quit will cause the media application
+ to attempt to quit (although it may still be prevented from quitting
+ by the user, for example).
+
+
+
+
+
+
+
+
+ Whether the media player is occupying the fullscreen.
+
+ This is typically used for videos. A value of true
+ indicates that the media player is taking up the full screen.
+
+
+ Media centre software may well have this value fixed to true
+
+
+ If CanSetFullscreen is true,
+ clients may set this property to true to tell the media player
+ to enter fullscreen mode, or to false to return to windowed
+ mode.
+
+
+ If CanSetFullscreen is false,
+ then attempting to set this property should have no effect, and may raise
+ an error. However, even if it is true, the media player
+ may still be unable to fulfil the request, in which case attempting to set
+ this property will have no effect (but should not raise an error).
+
+
+
+ This allows remote control interfaces, such as LIRC or mobile devices like
+ phones, to control whether a video is shown in fullscreen.
+
+
+
+
+
+
+
+
+
+
+ If false, attempting to set
+ Fullscreen will have no effect, and may
+ raise an error. If true, attempting to set
+ Fullscreen will not raise an error, and (if it
+ is different from the current value) will cause the media player to attempt to
+ enter or exit fullscreen mode.
+
+
+ Note that the media player may be unable to fulfil the request.
+ In this case, the value will not change. If the media player knows in
+ advance that it will not be able to fulfil the request, however, this
+ property should be false.
+
+
+
+ This allows clients to choose whether to display controls for entering
+ or exiting fullscreen mode.
+
+
+
+
+
+
+
+
+ If false, calling
+ Raise will have no effect, and may
+ raise a NotSupported error. If true, calling
+ Raise will cause the media application
+ to attempt to bring its user interface to the front, although it may
+ be prevented from doing so (by the window manager, for example).
+
+
+
+
+
+
+
+ Indicates whether the /org/mpris/MediaPlayer2
+ object implements the org.mpris.MediaPlayer2.TrackList
+ interface.
+
+
+
+
+
+
+ A friendly name to identify the media player to users.
+ This should usually match the name found in .desktop files
+ (eg: "VLC media player").
+
+
+
+
+
+
+ The basename of an installed .desktop file which complies with the Desktop entry specification,
+ with the ".desktop" extension stripped.
+
+ Example: The desktop entry file is "/usr/share/applications/vlc.desktop",
+ and this property contains "vlc"
+
+
+
+
+
+
+
+ The URI schemes supported by the media player.
+
+
+ This can be viewed as protocols supported by the player in almost
+ all cases. Almost every media player will include support for the
+ "file" scheme. Other common schemes are "http" and "rtsp".
+
+
+ Note that URI schemes should be lower-case.
+
+
+
+ This is important for clients to know when using the editing
+ capabilities of the Playlist interface, for example.
+
+
+
+
+
+
+
+
+ The mime-types supported by the media player.
+
+
+ Mime-types should be in the standard format (eg: audio/mpeg or
+ application/ogg).
+
+
+
+ This is important for clients to know when using the editing
+ capabilities of the Playlist interface, for example.
+
+
+
+
+
+
+
+
diff --git a/core/include/JellyfinQt/jellyfin.h b/core/include/JellyfinQt/jellyfin.h
index 4cc4612..d6aedf3 100644
--- a/core/include/JellyfinQt/jellyfin.h
+++ b/core/include/JellyfinQt/jellyfin.h
@@ -22,6 +22,7 @@
#include
#include "model/item.h"
+#include "dto/itemfields.h"
#include "dto/mediastream.h"
#include "dto/nameguidpair.h"
#include "dto/userdto.h"
@@ -36,6 +37,7 @@
#include "viewmodel/loader.h"
#include "viewmodel/mediastream.h"
#include "viewmodel/modelstatus.h"
+#include "viewmodel/platformmediacontrol.h"
#include "viewmodel/playbackmanager.h"
#include "viewmodel/playlist.h"
#include "viewmodel/userdata.h"
diff --git a/core/include/JellyfinQt/model/playlist.h b/core/include/JellyfinQt/model/playlist.h
index a33030e..32523db 100644
--- a/core/include/JellyfinQt/model/playlist.h
+++ b/core/include/JellyfinQt/model/playlist.h
@@ -58,11 +58,15 @@ public:
*/
int currentItemIndexInList() const;
+ bool hasPrevious();
+
/**
* @brief Determine the previous item to be played.
*/
void previous();
+ bool hasNext();
+
/**
* @brief Determine the next item to be played.
*/
diff --git a/core/include/JellyfinQt/model/shuffle.h b/core/include/JellyfinQt/model/shuffle.h
index 8c5b66d..bcaacc4 100644
--- a/core/include/JellyfinQt/model/shuffle.h
+++ b/core/include/JellyfinQt/model/shuffle.h
@@ -83,6 +83,9 @@ public:
*/
virtual int nextItem() const { return -1; }
+ virtual bool hasPrevious() const { return false; }
+ virtual bool hasNext() const { return false; }
+
/**
* @brief Sets whether the shuffler to loop over the list if all items are played.
*/
@@ -108,6 +111,8 @@ public:
virtual void previous() override;
virtual void next() override;
virtual void setIndex(int i) override;
+ virtual bool hasPrevious() const override;
+ virtual bool hasNext() const override;
protected:
int nextIndex() const;
int previousIndex() const;
@@ -148,6 +153,8 @@ public:
virtual int nextItem() const override;
virtual void previous() override;
virtual void next() override;
+ virtual bool hasPrevious() const override;
+ virtual bool hasNext() const override;
protected:
int m_previous, m_current, m_next = -1;
};
diff --git a/core/include/JellyfinQt/platform/freedesktop/mediaplayer2.h b/core/include/JellyfinQt/platform/freedesktop/mediaplayer2.h
new file mode 100644
index 0000000..d4e5e56
--- /dev/null
+++ b/core/include/JellyfinQt/platform/freedesktop/mediaplayer2.h
@@ -0,0 +1,118 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp org.mpris.MediaPlayer2.xml -a ../include/JellyfinQt/platform/freedesktop/mediaplayer2.h:../src/platform/freedesktop/mediaplayer2.cpp
+ *
+ * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd.
+ *
+ * This is an auto-generated file.
+ * This file may have been hand-edited. Look for HAND-EDIT comments
+ * before re-generating it.
+ */
+
+//HAND-EDIT: include-guard
+#ifndef JELLYFIN_PLATFORM_FREEDESKTOP_MEDIAPLAYER2_H
+#define JELLYFIN_PLATFORM_FREEDESKTKOP_MEDIAPLAYER2_H
+
+#include
+#include
+QT_BEGIN_NAMESPACE
+class QByteArray;
+template class QList;
+template class QMap;
+class QString;
+class QStringList;
+class QVariant;
+QT_END_NAMESPACE
+
+//HAND-EDIT: added namespaces
+namespace Jellyfin {
+namespace ViewModel {
+ class PlatformMediaControl;
+}
+
+namespace Platform {
+namespace FreeDesktop {
+
+/*
+ * Adaptor class for interface org.mpris.MediaPlayer2
+ */
+class MediaPlayer2Adaptor: public QDBusAbstractAdaptor
+{
+ Q_OBJECT
+ Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2")
+ Q_CLASSINFO("D-Bus Introspection", ""
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+ "")
+public:
+ MediaPlayer2Adaptor(ViewModel::PlatformMediaControl *parent);
+ virtual ~MediaPlayer2Adaptor();
+
+public: // PROPERTIES
+ Q_PROPERTY(bool CanQuit READ canQuit)
+ bool canQuit() const;
+
+ Q_PROPERTY(bool CanRaise READ canRaise)
+ bool canRaise() const;
+
+ Q_PROPERTY(bool CanSetFullscreen READ canSetFullscreen)
+ bool canSetFullscreen() const;
+
+ Q_PROPERTY(QString DesktopEntry READ desktopEntry)
+ QString desktopEntry() const;
+
+ Q_PROPERTY(bool Fullscreen READ fullscreen WRITE setFullscreen)
+ bool fullscreen() const;
+ void setFullscreen(bool value);
+
+ Q_PROPERTY(bool HasTrackList READ hasTrackList)
+ bool hasTrackList() const;
+
+ Q_PROPERTY(QString Identity READ identity)
+ QString identity() const;
+
+ Q_PROPERTY(QStringList SupportedMimeTypes READ supportedMimeTypes)
+ QStringList supportedMimeTypes() const;
+
+ Q_PROPERTY(QStringList SupportedUriSchemes READ supportedUriSchemes)
+ QStringList supportedUriSchemes() const;
+
+public Q_SLOTS: // METHODS
+ void Quit();
+ void Raise();
+Q_SIGNALS: // SIGNALS
+private:
+ ViewModel::PlatformMediaControl *m_mediaControl;
+};
+
+} // NS FreeDesktop
+} // NS Platform
+} // NS Jellyfin
+
+#endif
diff --git a/core/include/JellyfinQt/platform/freedesktop/mediaplayer2player.h b/core/include/JellyfinQt/platform/freedesktop/mediaplayer2player.h
new file mode 100644
index 0000000..5e0f95c
--- /dev/null
+++ b/core/include/JellyfinQt/platform/freedesktop/mediaplayer2player.h
@@ -0,0 +1,205 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp org.mpris.MediaPlayer2.Player.xml -a ../include/JellyfinQt/platform/freedesktop/mediaplayer2player.h:../src/platform/freedesktop/mediaplayer2player.cpp
+ *
+ * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd.
+ *
+ * This is an auto-generated file.
+ * This file may have been hand-edited. Look for HAND-EDIT comments
+ * before re-generating it.
+ */
+
+//HAND-EDIT: include-guard
+#ifndef JELLYFIN_PLATFORM_FREEDESKTOP_MEDIAPLAYER2PLAYER_H
+#define JELLYFIN_PLATFORM_FREEDESKTOP_MEDIAPLAYER2PLAYER_H
+
+#include
+#include
+#include
+QT_BEGIN_NAMESPACE
+class QByteArray;
+template class QList;
+template class QMap;
+class QString;
+class QStringList;
+class QVariant;
+QT_END_NAMESPACE
+
+//HAND-EDIT: added namespaces
+namespace Jellyfin {
+namespace ViewModel {
+ class Item;
+ class PlatformMediaControl;
+ class PlaybackManager;
+}
+
+namespace Platform {
+namespace FreeDesktop {
+/*
+ * Adaptor class for interface org.mpris.MediaPlayer2.Player
+ */
+class PlayerAdaptor: public QDBusAbstractAdaptor
+{
+ Q_OBJECT
+ Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2.Player")
+ Q_CLASSINFO("D-Bus Introspection", ""
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+" \n"
+ "")
+public:
+ PlayerAdaptor(ViewModel::PlatformMediaControl *parent);
+ virtual ~PlayerAdaptor();
+
+public: // PROPERTIES
+ Q_PROPERTY(bool CanControl READ canControl)
+ bool canControl() const;
+
+ Q_PROPERTY(bool CanGoNext READ canGoNext)
+ bool canGoNext() const;
+
+ Q_PROPERTY(bool CanGoPrevious READ canGoPrevious)
+ bool canGoPrevious() const;
+
+ Q_PROPERTY(bool CanPause READ canPause)
+ bool canPause() const;
+
+ Q_PROPERTY(bool CanPlay READ canPlay)
+ bool canPlay() const;
+
+ Q_PROPERTY(bool CanSeek READ canSeek)
+ bool canSeek() const;
+
+ Q_PROPERTY(QString LoopStatus READ loopStatus WRITE setLoopStatus)
+ QString loopStatus() const;
+ void setLoopStatus(const QString &value);
+
+ Q_PROPERTY(double MaximumRate READ maximumRate)
+ double maximumRate() const;
+
+ Q_PROPERTY(QVariantMap Metadata READ metadata)
+ QVariantMap metadata() const;
+
+ Q_PROPERTY(double MinimumRate READ minimumRate)
+ double minimumRate() const;
+
+ Q_PROPERTY(QString PlaybackStatus READ playbackStatus)
+ QString playbackStatus() const;
+
+ Q_PROPERTY(qlonglong Position READ position)
+ qlonglong position() const;
+
+ Q_PROPERTY(double Rate READ rate WRITE setRate)
+ double rate() const;
+ void setRate(double value);
+
+ Q_PROPERTY(bool Shuffle READ shuffle WRITE setShuffle)
+ bool shuffle() const;
+ void setShuffle(bool value);
+
+ Q_PROPERTY(double Volume READ volume WRITE setVolume)
+ double volume() const;
+ void setVolume(double value);
+
+public Q_SLOTS: // METHODS
+ void Next();
+ void OpenUri(const QString &Uri);
+ void Pause();
+ void Play();
+ void PlayPause();
+ void Previous();
+ void Seek(qlonglong Offset);
+ void SetPosition(const QDBusObjectPath &TrackId, qlonglong Position);
+ void Stop();
+Q_SIGNALS: // SIGNALS
+ void Seeked(qlonglong Position);
+
+private:
+ ViewModel::PlatformMediaControl *m_mediaControl;
+ void notifyPropertiesChanged(QStringList properties);
+private slots:
+ void onCurrentItemChanged(ViewModel::Item *newItem);
+ void onPlaybackStateChanged(QMediaPlayer::State state);
+ void onMediaStatusChanged(QMediaPlayer::MediaStatus status);
+ void onPositionChanged(qint64 position);
+ void onSeekableChanged(bool seekable);
+ void onPlaybackManagerChanged(ViewModel::PlaybackManager *newPlaybackManager);
+};
+
+} // NS FreeDesktop
+} // NS Platform
+} // NS Jellyfin
+
+#endif
diff --git a/core/include/JellyfinQt/viewmodel/platformmediacontrol.h b/core/include/JellyfinQt/viewmodel/platformmediacontrol.h
new file mode 100644
index 0000000..13fbe14
--- /dev/null
+++ b/core/include/JellyfinQt/viewmodel/platformmediacontrol.h
@@ -0,0 +1,127 @@
+/*
+ * Sailfin: a Jellyfin client written using Qt
+ * Copyright (C) 2021 Chris Josten and the Sailfin Contributors.
+ *
+ * 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
+ */
+#ifndef JELLYFIN_VIEWMODEL_PLATFORMMEDIACONTROL_H
+#define JELLYFIN_VIEWMODEL_PLATFORMMEDIACONTROL_H
+
+#include
+#include
+#include
+
+namespace Jellyfin {
+namespace ViewModel {
+
+class PlatformMediaControlPrivate;
+class PlaybackManager;
+
+/**
+ * @brief Exposes media control and information to the OS. Uses MPRIS on FreeDesktop-enabled systems.
+ */
+class PlatformMediaControl : public QObject, public QQmlParserStatus {
+ Q_OBJECT
+ Q_INTERFACES(QQmlParserStatus)
+public:
+ explicit PlatformMediaControl(QObject *parent = nullptr);
+
+ Q_PROPERTY(Jellyfin::ViewModel::PlaybackManager *playbackManager READ playbackManager WRITE setPlaybackManager NOTIFY playbackManagerChanged)
+ /**
+ * Whether the operating system can request the media player to quit. If set,
+ * the quitRequested signal may be emitted and the application should quit.
+ */
+ Q_PROPERTY(bool canQuit READ canQuit WRITE setCanQuit NOTIFY canQuitChanged)
+ Q_PROPERTY(bool canRaise READ canRaise WRITE setCanRaise NOTIFY canRaiseChanged)
+ Q_PROPERTY(QString playerName READ playerName WRITE setPlayerName NOTIFY playerNameChanged)
+ Q_PROPERTY(QString desktopFile READ playerName WRITE setPlayerName NOTIFY playerNameChanged)
+
+ PlaybackManager *playbackManager() const { return m_playbackManager; };
+
+ void setPlaybackManager(PlaybackManager *newPlaybackManager) {
+ m_playbackManager = newPlaybackManager;
+ emit playbackManagerChanged(newPlaybackManager);
+ };
+
+ bool canQuit() const { return m_canQuit; };
+ void setCanQuit(bool newCanQuit) {
+ m_canQuit = newCanQuit;
+ emit canQuitChanged(newCanQuit);
+ }
+
+ void requestQuit() {
+ emit quitRequested();
+ }
+
+ bool canRaise() const { return m_canRaise; };
+ void setCanRaise(bool newCanRaise) {
+ m_canRaise = newCanRaise;
+ emit canRaiseChanged(newCanRaise);
+ }
+
+ void requestRaise() {
+ emit raiseRequested();
+ };;
+
+ QString playerName() const { return m_playerName; }
+ void setPlayerName(QString newPlayerName) {
+ m_playerName = newPlayerName;
+ emit playerNameChanged(newPlayerName);
+ }
+
+ QString desktopFile() const { return m_desktopFile; }
+ void setDesktopFile(QString newDesktopFile) {
+ m_desktopFile = newDesktopFile;
+ emit desktopFileChanged(newDesktopFile);
+ }
+
+ void classBegin() override {
+ m_isParsing = true;
+ }
+ void componentComplete() override {
+ m_isParsing = false;
+ setup();
+ }
+
+
+
+signals:
+ void playbackManagerChanged(PlaybackManager *newPlaybackManager);
+ void canQuitChanged(bool newCanQuit);
+ void canRaiseChanged(bool newCanRaise);
+ void playerNameChanged(QString newPlayerName);
+ void desktopFileChanged(QString newDesktopFile);
+
+ void quitRequested();
+ void raiseRequested();
+
+private:
+ Q_DECLARE_PRIVATE(PlatformMediaControl)
+ PlatformMediaControlPrivate* d_ptr;
+ void setup();
+ bool m_isParsing = false;
+
+ PlaybackManager *m_playbackManager = nullptr;
+ bool m_canQuit = false;
+ bool m_canRaise = false;
+ QString m_playerName = QStringLiteral("JellyfinQt");
+ QString m_desktopFile = QStringLiteral("sailfin");
+};
+
+
+} // NS ViewModel
+} // NS Jellyfin
+
+#endif // PLATFORMMEDIACONTROL_H
diff --git a/core/include/JellyfinQt/viewmodel/playbackmanager.h b/core/include/JellyfinQt/viewmodel/playbackmanager.h
index be324a3..39173fc 100644
--- a/core/include/JellyfinQt/viewmodel/playbackmanager.h
+++ b/core/include/JellyfinQt/viewmodel/playbackmanager.h
@@ -97,8 +97,12 @@ public:
Q_PROPERTY(QMediaPlayer::MediaStatus mediaStatus READ mediaStatus NOTIFY mediaStatusChanged)
Q_PROPERTY(QMediaPlayer::State playbackState READ playbackState NOTIFY playbackStateChanged)
Q_PROPERTY(qint64 position READ position NOTIFY positionChanged)
+ Q_PROPERTY(bool hasNext READ hasNext NOTIFY hasNextChanged)
+ Q_PROPERTY(bool hasPrevious READ hasPrevious NOTIFY hasPreviousChanged)
ViewModel::Item *item() const { return m_displayItem; }
+ QSharedPointer dataItem() const { return m_item; }
+ ApiClient *apiClient() const { return m_apiClient; }
void setApiClient(ApiClient *apiClient);
QString streamUrl() const { return m_streamUrl; }
@@ -108,6 +112,8 @@ public:
qint64 duration() const { return m_mediaPlayer->duration(); }
ViewModel::Playlist *queue() const { return m_displayQueue; }
int queueIndex() const { return m_queueIndex; }
+ bool hasNext() const { return m_queue->hasNext(); }
+ bool hasPrevious() const { return m_queue->hasPrevious(); }
// Current media player related property getters
QMediaPlayer::State playbackState() const { return m_mediaPlayer->state()/*m_playbackState*/; }
@@ -138,6 +144,8 @@ signals:
void seekableChanged(bool newSeekable);
void errorChanged(QMediaPlayer::Error newError);
void errorStringChanged(const QString &newErrorString);
+ void hasNextChanged(bool newHasNext);
+ void hasPreviousChanged(bool newHasPrevious);
public slots:
/**
* @brief playItem Replaces the current queue and plays the item with the given id.
diff --git a/core/src/jellyfin.cpp b/core/src/jellyfin.cpp
index 9b30de5..45f6753 100644
--- a/core/src/jellyfin.cpp
+++ b/core/src/jellyfin.cpp
@@ -23,6 +23,7 @@ void registerTypes(const char *uri) {
qmlRegisterType(uri, 1, 0, "ApiClient");
qmlRegisterType(uri, 1, 0, "ServerDiscoveryModel");
qmlRegisterType(uri, 1, 0, "PlaybackManager");
+ qmlRegisterType(uri, 1, 0, "PlatformMediaControl");
qmlRegisterUncreatableType(uri, 1, 0, "Item", "Acquire one via ItemLoader or exposed properties");
qmlRegisterUncreatableType(uri, 1, 0, "User", "Acquire one via UserLoader or exposed properties");
qmlRegisterUncreatableType(uri, 1, 0, "EventBus", "Obtain one via your ApiClient");
diff --git a/core/src/model/playlist.cpp b/core/src/model/playlist.cpp
index 6c37f13..561222b 100644
--- a/core/src/model/playlist.cpp
+++ b/core/src/model/playlist.cpp
@@ -35,6 +35,10 @@ void Playlist::clearList() {
emit listCleared();
}
+bool Playlist::hasPrevious() {
+ return m_shuffler->hasPrevious();
+}
+
void Playlist::previous() {
m_shuffler->previous();
int curItem = m_shuffler->currentItem();
@@ -54,6 +58,10 @@ void Playlist::previous() {
emit currentItemChanged();
}
+bool Playlist::hasNext() {
+ return m_shuffler->hasNext();
+}
+
void Playlist::next() {
// Determine the new current item
if (!m_queue.isEmpty()) {
diff --git a/core/src/model/shuffle.cpp b/core/src/model/shuffle.cpp
index b6ee65b..831af08 100644
--- a/core/src/model/shuffle.cpp
+++ b/core/src/model/shuffle.cpp
@@ -44,6 +44,14 @@ void NoShuffle::previous() {
m_index = previousIndex();
}
+bool NoShuffle::hasPrevious() const {
+ return m_index > 0;
+}
+
+bool NoShuffle::hasNext() const {
+ return m_index < m_playlist->listSize() - 1;
+}
+
int NoShuffle::currentItem() const {
return m_index;
}
@@ -150,5 +158,13 @@ void RandomShuffle::next() {
m_next = random(m_playlist->listSize());
}
+bool RandomShuffle::hasPrevious() const {
+ return true;
+}
+
+bool RandomShuffle::hasNext() const {
+ return true;
+}
+
} // NS Model
} // NS Jellyfin
diff --git a/core/src/platform/freedesktop/mediaplayer2.cpp b/core/src/platform/freedesktop/mediaplayer2.cpp
new file mode 100644
index 0000000..7f67859
--- /dev/null
+++ b/core/src/platform/freedesktop/mediaplayer2.cpp
@@ -0,0 +1,117 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp org.mpris.MediaPlayer2.xml -a ../include/JellyfinQt/platform/freedesktop/mediaplayer2.h:../src/platform/freedesktop/mediaplayer2.cpp
+ *
+ * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd.
+ *
+ * This is an auto-generated file.
+ * Do not edit! All changes made to it will be lost.
+ */
+
+#include "JellyfinQt/platform/freedesktop/mediaplayer2.h"
+#include "JellyfinQt/viewmodel/platformmediacontrol.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Jellyfin {
+namespace Platform {
+namespace FreeDesktop {
+
+/*
+ * Implementation of adaptor class MediaPlayer2Adaptor
+ */
+
+MediaPlayer2Adaptor::MediaPlayer2Adaptor(ViewModel::PlatformMediaControl *parent)
+ : QDBusAbstractAdaptor(parent),
+ m_mediaControl(parent)
+{
+ // constructor
+ setAutoRelaySignals(true);
+}
+
+MediaPlayer2Adaptor::~MediaPlayer2Adaptor()
+{
+ // destructor
+}
+
+bool MediaPlayer2Adaptor::canQuit() const
+{
+ // get the value of property CanQuit
+ return m_mediaControl->canQuit();
+}
+
+bool MediaPlayer2Adaptor::canRaise() const
+{
+ // get the value of property CanRaise
+ return m_mediaControl->canRaise();
+}
+
+bool MediaPlayer2Adaptor::canSetFullscreen() const
+{
+ // get the value of property CanSetFullscreen
+ return qvariant_cast< bool >(parent()->property("CanSetFullscreen"));
+}
+
+QString MediaPlayer2Adaptor::desktopEntry() const
+{
+ // get the value of property DesktopEntry
+ return m_mediaControl->desktopFile();
+}
+
+bool MediaPlayer2Adaptor::fullscreen() const
+{
+ // get the value of property Fullscreen
+ return qvariant_cast< bool >(parent()->property("Fullscreen"));
+}
+
+void MediaPlayer2Adaptor::setFullscreen(bool value)
+{
+ // set the value of property Fullscreen
+ parent()->setProperty("Fullscreen", QVariant::fromValue(value));
+}
+
+bool MediaPlayer2Adaptor::hasTrackList() const
+{
+ // get the value of property HasTrackList
+ //return qvariant_cast< bool >(parent()->property("HasTrackList"));
+ return false;
+}
+
+QString MediaPlayer2Adaptor::identity() const
+{
+ // get the value of property Identity
+ return m_mediaControl->playerName();
+}
+
+QStringList MediaPlayer2Adaptor::supportedMimeTypes() const
+{
+ // get the value of property SupportedMimeTypes
+ return qvariant_cast< QStringList >(parent()->property("SupportedMimeTypes"));
+}
+
+QStringList MediaPlayer2Adaptor::supportedUriSchemes() const
+{
+ // get the value of property SupportedUriSchemes
+ QStringList supportedUriSchemes;
+ supportedUriSchemes << "urn";
+ return supportedUriSchemes;
+}
+
+void MediaPlayer2Adaptor::Quit()
+{
+ m_mediaControl->requestQuit();
+}
+
+void MediaPlayer2Adaptor::Raise()
+{
+ m_mediaControl->requestRaise();
+}
+
+} // NS FreeDesktop
+} // NS Platform
+} // NS Jellyfin
diff --git a/core/src/platform/freedesktop/mediaplayer2player.cpp b/core/src/platform/freedesktop/mediaplayer2player.cpp
new file mode 100644
index 0000000..9a477cb
--- /dev/null
+++ b/core/src/platform/freedesktop/mediaplayer2player.cpp
@@ -0,0 +1,325 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp org.mpris.MediaPlayer2.Player.xml -a ../include/JellyfinQt/platform/freedesktop/mediaplayer2player.h:../src/platform/freedesktop/mediaplayer2player.cpp
+ *
+ * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd.
+ *
+ * This is an auto-generated file.
+ * Do not edit! All changes made to it will be lost.
+ */
+
+#include "JellyfinQt/platform/freedesktop/mediaplayer2player.h"
+#include "JellyfinQt/viewmodel/item.h"
+#include "JellyfinQt/viewmodel/platformmediacontrol.h"
+#include "JellyfinQt/viewmodel/playbackmanager.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Jellyfin {
+namespace Platform {
+namespace FreeDesktop {
+
+/*
+ * Implementation of adaptor class PlayerAdaptor
+ */
+
+PlayerAdaptor::PlayerAdaptor(ViewModel::PlatformMediaControl *parent)
+ : QDBusAbstractAdaptor(parent),
+ m_mediaControl(parent) {
+ // constructor
+ //setAutoRelaySignals(true);
+ onPlaybackManagerChanged(m_mediaControl->playbackManager());
+ connect(m_mediaControl, &ViewModel::PlatformMediaControl::playbackManagerChanged, this, &PlayerAdaptor::onPlaybackManagerChanged);
+ /*if (m_mediaControl != nullptr && m_mediaControl->playbackManager() != nullptr) {
+ }*/
+}
+
+PlayerAdaptor::~PlayerAdaptor() {
+ // destructor
+}
+
+bool PlayerAdaptor::canControl() const
+{
+ // get the value of property CanControl
+ return true;
+}
+
+bool PlayerAdaptor::canGoNext() const
+{
+ // get the value of property CanGoNext
+ return canPlay() && m_mediaControl->playbackManager()->hasNext();
+}
+
+bool PlayerAdaptor::canGoPrevious() const
+{
+ // get the value of property CanGoPrevious
+ return canPlay() && m_mediaControl->playbackManager()->hasPrevious();
+}
+
+bool PlayerAdaptor::canPause() const
+{
+ // get the value of property CanPause
+ return canPlay();
+}
+
+bool PlayerAdaptor::canPlay() const
+{
+ // get the value of property CanPlay
+ return m_mediaControl->playbackManager()->queue()->rowCount(QModelIndex()) > 0;
+}
+
+bool PlayerAdaptor::canSeek() const
+{
+ // get the value of property CanSeek
+ return m_mediaControl->playbackManager()->seekable();
+}
+
+QString PlayerAdaptor::loopStatus() const
+{
+ // get the value of property LoopStatus
+ return "None";
+}
+
+void PlayerAdaptor::setLoopStatus(const QString &value)
+{
+ // set the value of property LoopStatus
+ parent()->setProperty("LoopStatus", QVariant::fromValue(value));
+}
+
+double PlayerAdaptor::maximumRate() const
+{
+ // get the value of property MaximumRate
+ return 1.0;
+}
+
+QVariantMap PlayerAdaptor::metadata() const
+{
+ // get the value of property Metadata
+ QVariantMap map;
+ if (m_mediaControl->playbackManager() == nullptr || m_mediaControl->playbackManager()->dataItem().isNull()) {
+ return map;
+ }
+ ViewModel::PlaybackManager *plybkMgr = m_mediaControl->playbackManager();
+
+ QSharedPointer item = plybkMgr->dataItem();
+ if (!item.isNull()) {
+ map[QStringLiteral("mpris:trackid")] = QVariant::fromValue(QDBusObjectPath(QStringLiteral("/nl/netsoj/chris/jellyfinqt/item/").append(item->jellyfinId())));
+ if (item->runTimeTicks().has_value()) {
+ map[QStringLiteral("mpris:length")] = item->runTimeTicks().value() / 10;
+ }
+ map[QStringLiteral("xesam:title")] = item->name();
+ if (!item->albumPrimaryImageTagNull()) {
+ map[QStringLiteral("mpris:artUrl")] = QStringLiteral("%1/Items/%2/Images/Primary?tag=%3").arg(plybkMgr->apiClient()->baseUrl(),
+ item->jellyfinId(), item->albumPrimaryImageTag());
+ }
+
+ QStringList albumArtists;
+ QList tmp = item->albumArtists();
+ for (auto it = tmp.cbegin(); it != tmp.cend(); it++) {
+ albumArtists << it->name();
+ }
+ map[QStringLiteral("xesam:albumArtist")] = albumArtists;
+ map[QStringLiteral("xesam:album")] = item->album();
+ map[QStringLiteral("xesam:artist")] = item->artists();
+ if (item->parentIndexNumber().has_value()) {
+ map[QStringLiteral("xesam:discNumber")] = item->parentIndexNumber().value();
+ }
+ if (item->indexNumber().has_value()) {
+ map[QStringLiteral("xesam:trackNumber")] = item->indexNumber().value();
+ }
+ map[QStringLiteral("xesam:contentCreated")] = item->dateCreated();
+ map[QStringLiteral("xesam:genre")] = item->genres();
+ map[QStringLiteral("xesam:lastUsed")] = item->userData()->lastPlayedDate();
+ }
+ return map;
+}
+
+double PlayerAdaptor::minimumRate() const
+{
+ // get the value of property MinimumRate
+ return 1.0;
+}
+
+QString PlayerAdaptor::playbackStatus() const
+{
+ // get the value of property PlaybackStatus
+ if (m_mediaControl == nullptr || m_mediaControl->playbackManager() == nullptr) {
+ return "Stopped";
+ }
+ switch(m_mediaControl->playbackManager()->playbackState()) {
+ case QMediaPlayer::StoppedState:
+ return "Stopped";
+ case QMediaPlayer::PlayingState:
+ return "Playing";
+ case QMediaPlayer::PausedState:
+ return "Paused";
+ default:
+ return "Stopped";
+ }
+}
+
+qlonglong PlayerAdaptor::position() const
+{
+ // get the value of property Position
+ return m_mediaControl->playbackManager()->position() * 1000;
+}
+
+double PlayerAdaptor::rate() const
+{
+ // get the value of property Rate
+ return 1.0;
+}
+
+void PlayerAdaptor::setRate(double value)
+{
+ // set the value of property Rate
+ parent()->setProperty("Rate", QVariant::fromValue(value));
+}
+
+bool PlayerAdaptor::shuffle() const
+{
+ // get the value of property Shuffle
+ return false;
+}
+
+void PlayerAdaptor::setShuffle(bool value)
+{
+ // set the value of property Shuffle
+ parent()->setProperty("Shuffle", QVariant::fromValue(value));
+}
+
+double PlayerAdaptor::volume() const
+{
+ // get the value of property Volume
+ return qvariant_cast< double >(parent()->property("Volume"));
+}
+
+void PlayerAdaptor::setVolume(double value)
+{
+ // set the value of property Volume
+ parent()->setProperty("Volume", QVariant::fromValue(value));
+}
+
+void PlayerAdaptor::Next()
+{
+ // handle method call org.mpris.MediaPlayer2.Player.Next
+ m_mediaControl->playbackManager()->next();
+}
+
+void PlayerAdaptor::OpenUri(const QString &Uri)
+{
+ // handle method call org.mpris.MediaPlayer2.Player.OpenUri
+ QMetaObject::invokeMethod(parent(), "OpenUri", Q_ARG(QString, Uri));
+}
+
+void PlayerAdaptor::Pause()
+{
+ // handle method call org.mpris.MediaPlayer2.Player.Pause
+ m_mediaControl->playbackManager()->pause();
+}
+
+void PlayerAdaptor::Play()
+{
+ // handle method call org.mpris.MediaPlayer2.Player.Play
+ m_mediaControl->playbackManager()->play();
+}
+
+void PlayerAdaptor::PlayPause()
+{
+ // handle method call org.mpris.MediaPlayer2.Player.PlayPause
+ if (m_mediaControl->playbackManager()->playbackState() == QMediaPlayer::PlayingState) {
+ m_mediaControl->playbackManager()->pause();
+ } else {
+ m_mediaControl->playbackManager()->play();
+ }
+}
+
+void PlayerAdaptor::Previous()
+{
+ // handle method call org.mpris.MediaPlayer2.Player.Previous
+ m_mediaControl->playbackManager()->previous();
+}
+
+void PlayerAdaptor::Seek(qlonglong Offset)
+{
+ // handle method call org.mpris.MediaPlayer2.Player.Seek
+ m_mediaControl->playbackManager()->seek(m_mediaControl->playbackManager()->position() + Offset / 1000);
+}
+
+void PlayerAdaptor::SetPosition(const QDBusObjectPath &TrackId, qlonglong Position)
+{
+ // handle method call org.mpris.MediaPlayer2.Player.SetPosition
+ if (TrackId.path() == QStringLiteral("/nl/netsoj/chris/jellyfinqt/item/").append(m_mediaControl->playbackManager()->dataItem()->jellyfinId())) {
+ m_mediaControl->playbackManager()->seek(Position / 1000);
+ }
+}
+
+void PlayerAdaptor::Stop()
+{
+ // handle method call org.mpris.MediaPlayer2.Player.Stop
+ QMetaObject::invokeMethod(parent(), "Stop");
+}
+void PlayerAdaptor::notifyPropertiesChanged(QStringList properties) {
+ QDBusMessage signal = QDBusMessage::createSignal("/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged");
+ signal << "org.mpris.MediaPlayer2.Player"; // 1st argument: interface name
+ QVariantMap changedProperties;
+ for (auto it = properties.cbegin(); it != properties.cend(); it++) {
+ changedProperties[*it] = property(it->toLocal8Bit().data());
+ }
+ signal << changedProperties; // 2nd argument: changed properties
+ signal << QStringList(); // 3th argument: invalidated properties
+ QDBusConnection::sessionBus().send(signal);
+}
+
+void PlayerAdaptor::onCurrentItemChanged(ViewModel::Item *item) {
+ Q_UNUSED(item)
+
+ QStringList properties;
+ properties << "Metadata" << "Position" << "CanPlay" << "CanPause" << "CanGoNext" << "CanGoPrevious";
+ notifyPropertiesChanged(properties);
+}
+void PlayerAdaptor::onPlaybackStateChanged(QMediaPlayer::State state) {
+ Q_UNUSED(state)
+ QStringList properties;
+ properties << "PlaybackStatus" << "Position";
+ notifyPropertiesChanged(properties);
+
+}
+
+void PlayerAdaptor::onMediaStatusChanged(QMediaPlayer::MediaStatus status) {
+ Q_UNUSED(status)
+ QStringList properties;
+ properties << "PlaybackStatus" << "Position";
+ notifyPropertiesChanged(properties);
+}
+
+void PlayerAdaptor::onPositionChanged(qint64 position) {
+ Q_UNUSED(position)
+ /*QStringList properties;
+ properties << "Position";
+ notifyPropertiesChanged(properties);*/
+}
+void PlayerAdaptor::onSeekableChanged(bool seekable) {
+ QStringList properties;
+ properties << "CanSeek";
+ notifyPropertiesChanged(properties);
+}
+
+void PlayerAdaptor::onPlaybackManagerChanged(ViewModel::PlaybackManager *newPlaybackManager) {
+ if (newPlaybackManager != nullptr) {
+ connect(newPlaybackManager, &ViewModel::PlaybackManager::itemChanged, this, &PlayerAdaptor::onCurrentItemChanged);
+ connect(newPlaybackManager, &ViewModel::PlaybackManager::playbackStateChanged, this, &PlayerAdaptor::onPlaybackStateChanged);
+ connect(newPlaybackManager, &ViewModel::PlaybackManager::mediaStatusChanged, this, &PlayerAdaptor::onMediaStatusChanged);
+ connect(newPlaybackManager, &ViewModel::PlaybackManager::positionChanged, this, &PlayerAdaptor::onPositionChanged);
+ connect(newPlaybackManager, &ViewModel::PlaybackManager::seekableChanged, this, &PlayerAdaptor::onSeekableChanged);
+ }
+}
+
+} // NS FreeDesktop
+} // NS Platform
+} // NS Jellyfin
diff --git a/core/src/viewmodel/platformmediacontrol_freedesktop.cpp b/core/src/viewmodel/platformmediacontrol_freedesktop.cpp
new file mode 100644
index 0000000..d6548ed
--- /dev/null
+++ b/core/src/viewmodel/platformmediacontrol_freedesktop.cpp
@@ -0,0 +1,87 @@
+/*
+ * Sailfin: a Jellyfin client written using Qt
+ * Copyright (C) 2021 Chris Josten and the Sailfin Contributors.
+ *
+ * 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
+ */
+
+#include "JellyfinQt/viewmodel/platformmediacontrol.h"
+#include "JellyfinQt/platform/freedesktop/mediaplayer2.h"
+#include "JellyfinQt/platform/freedesktop/mediaplayer2player.h"
+
+#include
+#include
+#include
+
+namespace Jellyfin {
+namespace ViewModel {
+
+using Platform::FreeDesktop::MediaPlayer2Adaptor;
+using Platform::FreeDesktop::PlayerAdaptor;
+
+class PlatformMediaControlPrivate {
+public:
+ PlatformMediaControlPrivate(PlatformMediaControl *parent);
+ void setupConnection();
+private:
+ PlatformMediaControl *q_ptr;
+ Q_DECLARE_PUBLIC(PlatformMediaControl)
+
+ MediaPlayer2Adaptor *m_mainAdaptor;
+ PlayerAdaptor *m_playerAdaptor;
+ QDBusConnection m_connection;
+public slots:
+ // MPRIS Player methods
+ void Quit();
+
+};
+
+PlatformMediaControl::PlatformMediaControl(QObject *parent)
+ : QObject(parent) {
+ d_ptr = new PlatformMediaControlPrivate(this);
+}
+
+void PlatformMediaControl::setup() {
+ Q_D(PlatformMediaControl);
+ d->setupConnection();
+}
+
+PlatformMediaControlPrivate::PlatformMediaControlPrivate(PlatformMediaControl *parent)
+ : q_ptr(parent),
+ m_mainAdaptor(new MediaPlayer2Adaptor(parent)),
+ m_playerAdaptor(new PlayerAdaptor(parent)),
+ m_connection(QDBusConnection::sessionBus()) {
+
+}
+
+void PlatformMediaControlPrivate::setupConnection() {
+ Q_Q(PlatformMediaControl);
+ if(!m_connection.registerObject(QStringLiteral("/org/mpris/MediaPlayer2"), q)) {
+ qWarning() << "MediaPlayer2 dbus object not registered: " << m_connection.lastError();
+ }
+ if (!m_connection.registerService(QStringLiteral("org.mpris.MediaPlayer2.sailfin.instance").append(QString::number(QCoreApplication::applicationPid())))) {
+ qWarning() << "Could not aqcuire DBus name: " << m_connection.lastError();
+ }
+}
+
+void PlatformMediaControlPrivate::Quit() {
+ Q_Q(PlatformMediaControl);
+ q->requestQuit();
+}
+
+
+
+}
+}
diff --git a/core/src/viewmodel/platformmediacontrol_stub.cpp b/core/src/viewmodel/platformmediacontrol_stub.cpp
new file mode 100644
index 0000000..241baac
--- /dev/null
+++ b/core/src/viewmodel/platformmediacontrol_stub.cpp
@@ -0,0 +1,29 @@
+/*
+ * Sailfin: a Jellyfin client written using Qt
+ * Copyright (C) 2021 Chris Josten and the Sailfin Contributors.
+ *
+ * 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
+ */
+
+#include "JellyfinQt/viewmodel/platformmediacontrol.h"
+
+namespace Jellyfin {
+namespace ViewModel {
+
+PlatformMediaControl::PlatformMediaControl(QObject *parent)
+ : QObject(parent) {}
+
+}
+}
diff --git a/core/src/viewmodel/playbackmanager.cpp b/core/src/viewmodel/playbackmanager.cpp
index 59fc5a0..902b531 100644
--- a/core/src/viewmodel/playbackmanager.cpp
+++ b/core/src/viewmodel/playbackmanager.cpp
@@ -84,6 +84,9 @@ void PlaybackManager::setItem(QSharedPointer newItem) {
}
emit itemChanged(m_displayItem);
+ emit hasNextChanged(m_queue->hasNext());
+ emit hasPreviousChanged(m_queue->hasPrevious());
+
if (m_apiClient == nullptr) {
qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
return;
diff --git a/qtquick/qml/main.qml b/qtquick/qml/main.qml
index 6e98d8f..474d9eb 100644
--- a/qtquick/qml/main.qml
+++ b/qtquick/qml/main.qml
@@ -16,13 +16,23 @@ ApplicationWindow {
height: 600
visible: true
property int _oldDepth: 0
- property alias playbackManager: playbackManager
+ property alias playbackManager: _playbackManager
J.PlaybackManager {
- id: playbackManager
+ id: _playbackManager
apiClient: ApiClient
}
+ J.PlatformMediaControl {
+ playbackManager: appWindow.playbackManager
+ canQuit: true
+ onQuitRequested: appWindow.close()
+ desktopFile: "sailfin"
+ playerName: "Sailfin"
+ canRaise: true
+ onRaiseRequested: appWindow.raise()
+ }
+
background: Background {
id: background
anchors.fill: parent
@@ -77,6 +87,7 @@ ApplicationWindow {
anchors.verticalCenter: parent.verticalCenter
text: "Previous"
onClicked: playbackManager.previous();
+ enabled: playbackManager.hasPrevious
}
Button {
readonly property bool _playing: playbackManager.playbackState === MediaPlayer.PlayingState;
@@ -88,6 +99,7 @@ ApplicationWindow {
anchors.verticalCenter: parent.verticalCenter
text: "Next"
onClicked: playbackManager.next();
+ enabled: playbackManager.hasNext
}
}
diff --git a/sailfish/qml/harbour-sailfin.qml b/sailfish/qml/harbour-sailfin.qml
index e3091aa..73c0995 100644
--- a/sailfish/qml/harbour-sailfin.qml
+++ b/sailfish/qml/harbour-sailfin.qml
@@ -51,6 +51,15 @@ ApplicationWindow {
supportedCommands: [GeneralCommandType.Play, GeneralCommandType.DisplayContent, GeneralCommandType.DisplayMessage]
}
+ PlatformMediaControl {
+ playbackManager: appWindow.playbackManager
+ canQuit: fasle
+ desktopFile: "harbour-sailfin"
+ playerName: "Sailfin"
+ canRaise: true
+ onRaiseRequested: appWindow.raise()
+ }
+
initialPage: Component {
MainPage {
Connections {