diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 3b444bc..e01a21d 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -13,12 +13,14 @@ include(GNUInstallDirs) include(GeneratedSources.cmake) set(JellyfinQt_SOURCES + src/model/controllablesession.cpp src/model/deviceprofile.cpp src/model/item.cpp src/model/player.cpp src/model/playbackmanager.cpp src/model/playbackreporter.cpp src/model/playlist.cpp + src/model/remotejellyfinplayback.cpp src/model/shuffle.cpp src/model/user.cpp @@ -32,6 +34,7 @@ set(JellyfinQt_SOURCES src/viewmodel/modelstatus.cpp src/viewmodel/playbackmanager.cpp src/viewmodel/playlist.cpp + src/viewmodel/remotedevice.cpp src/viewmodel/settings.cpp src/viewmodel/userdata.cpp src/viewmodel/usermodel.cpp @@ -49,41 +52,44 @@ set(JellyfinQt_SOURCES list(APPEND JellyfinQt_SOURCES ${openapi_SOURCES}) set(JellyfinQt_HEADERS - include/JellyfinQt/model/deviceprofile.h - include/JellyfinQt/model/item.h - include/JellyfinQt/model/player.h - include/JellyfinQt/model/playbackmanager.h - include/JellyfinQt/model/playbackreporter.h - include/JellyfinQt/model/playlist.h - include/JellyfinQt/model/shuffle.h - include/JellyfinQt/model/user.h - include/JellyfinQt/support/jsonconv.h - include/JellyfinQt/support/jsonconvimpl.h - include/JellyfinQt/support/loader.h - include/JellyfinQt/support/parseexception.h - include/JellyfinQt/viewmodel/item.h - include/JellyfinQt/viewmodel/itemmodel.h - include/JellyfinQt/viewmodel/loader.h - include/JellyfinQt/viewmodel/mediastream.h - 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/settings.h - include/JellyfinQt/viewmodel/userdata.h - include/JellyfinQt/viewmodel/usermodel.h - include/JellyfinQt/viewmodel/user.h - include/JellyfinQt/viewmodel/utils.h - include/JellyfinQt/apiclient.h - include/JellyfinQt/apimodel.h - include/JellyfinQt/credentialmanager.h - include/JellyfinQt/eventbus.h - include/JellyfinQt/jellyfin.h - include/JellyfinQt/jsonhelper.h - include/JellyfinQt/qobjectsettingswrapper.h - include/JellyfinQt/serverdiscoverymodel.h - include/JellyfinQt/websocket.h) + include/JellyfinQt/model/controllablesession.h + include/JellyfinQt/model/deviceprofile.h + include/JellyfinQt/model/item.h + include/JellyfinQt/model/player.h + include/JellyfinQt/model/playbackmanager.h + include/JellyfinQt/model/playbackreporter.h + include/JellyfinQt/model/playlist.h + include/JellyfinQt/model/remotejellyfinplayback.h + include/JellyfinQt/model/shuffle.h + include/JellyfinQt/model/user.h + include/JellyfinQt/support/jsonconv.h + include/JellyfinQt/support/jsonconvimpl.h + include/JellyfinQt/support/loader.h + include/JellyfinQt/support/parseexception.h + include/JellyfinQt/viewmodel/item.h + include/JellyfinQt/viewmodel/itemmodel.h + include/JellyfinQt/viewmodel/loader.h + include/JellyfinQt/viewmodel/mediastream.h + 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/remotedevice.h + include/JellyfinQt/viewmodel/settings.h + include/JellyfinQt/viewmodel/userdata.h + include/JellyfinQt/viewmodel/usermodel.h + include/JellyfinQt/viewmodel/user.h + include/JellyfinQt/viewmodel/utils.h + include/JellyfinQt/apiclient.h + include/JellyfinQt/apimodel.h + include/JellyfinQt/credentialmanager.h + include/JellyfinQt/eventbus.h + include/JellyfinQt/jellyfin.h + include/JellyfinQt/jsonhelper.h + include/JellyfinQt/qobjectsettingswrapper.h + include/JellyfinQt/serverdiscoverymodel.h + include/JellyfinQt/websocket.h) if (FREEDESKTOP_INTEGRATION) list(APPEND JellyfinQt_SOURCES diff --git a/core/include/JellyfinQt/apiclient.h b/core/include/JellyfinQt/apiclient.h index a96a5ed..d5cbbe6 100644 --- a/core/include/JellyfinQt/apiclient.h +++ b/core/include/JellyfinQt/apiclient.h @@ -96,6 +96,7 @@ public: explicit ApiClient(QObject *parent = nullptr); virtual ~ApiClient(); Q_PROPERTY(QString baseUrl READ baseUrl WRITE setBaseUrl NOTIFY baseUrlChanged) + Q_PROPERTY(QString appName READ appName WRITE setAppName NOTIFY appNameChanged) Q_PROPERTY(bool authenticated READ authenticated WRITE setAuthenticated NOTIFY authenticatedChanged) Q_PROPERTY(QString userId READ userId NOTIFY userIdChanged) Q_PROPERTY(QJsonObject deviceProfile READ deviceProfileJson NOTIFY deviceProfileChanged) @@ -114,6 +115,7 @@ public: bool authenticated() const; void setBaseUrl(const QString &url); + void setAppName(const QString &appName); QNetworkReply *get(const QString &path, const QUrlQuery ¶ms = QUrlQuery()); QNetworkReply *post(const QString &path, const QJsonDocument &data, const QUrlQuery ¶ms = QUrlQuery()); QNetworkReply *post(const QString &path, const QByteArray &data = QByteArray(), const QUrlQuery ¶ms = QUrlQuery()); @@ -127,6 +129,7 @@ public: Q_ENUM(ApiError) const QString &baseUrl() const; + const QString &appName() const; const QString &userId() const; const QString &deviceId() const; /** @@ -185,6 +188,7 @@ signals: void authenticatedChanged(bool authenticated); void baseUrlChanged(const QString &baseUrl); + void appNameChanged(const QString &newAppName); void settingsChanged(); /** diff --git a/core/include/JellyfinQt/model/controllablesession.h b/core/include/JellyfinQt/model/controllablesession.h new file mode 100644 index 0000000..3860b48 --- /dev/null +++ b/core/include/JellyfinQt/model/controllablesession.h @@ -0,0 +1,171 @@ +#ifndef JELLYFIN_MODEL_CONTROLLABLESESSION_H +#define JELLYFIN_MODEL_CONTROLLABLESESSION_H + +#include +#include +#include + +#include "JellyfinQt/dto/sessioninfo.h" + +namespace Jellyfin { + +class ApiClient; + +namespace DTO { +class ClientCapabilities; +} // NS DTO + +namespace Model { + +class PlaybackManager; + +class DeviceTypeClass { Q_GADGET +public: + enum Value { + Unknown, + Tv, + Computer, + Phone + }; + Q_ENUM(Value) +}; + +class MediaTypeClass { + Q_GADGET +public: + enum Value { + None = 0x0, + Audio = 0x1, + Video = 0x2, + Photo = 0x4 + }; + Q_DECLARE_FLAGS(MediaTypes, Value) +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(MediaTypeClass::MediaTypes) + +using DeviceType = DeviceTypeClass::Value; +using MediaTypes = MediaTypeClass::MediaTypes; + +/** + * @brief Abstract class for describing a playback session that can be controlled. + * + * Main purpose for this class is to hold information for displaying it in a UI for an user to select + * and to create an implementation of a PlaybackManager instance to control this session. + */ +class ControllableSession : public QObject { + Q_OBJECT +public: + explicit ControllableSession(QObject *parent = nullptr); + /** + * @brief An unique id for this session. + */ + virtual QString id() const = 0; + /** + * @brief An human-readable name for this session + */ + virtual QString name() const = 0; + /** + * @brief The app for this session + */ + virtual QString appName() const = 0; + virtual DeviceType deviceType() const = 0; + + /** + * @brief user The username of who started this session + */ + virtual QString userName() const = 0; + /** + * @brief Creates a playbackManager for this device. This PlaybackManager has no + * QObject parent and must be cleaned up by the caller. + */ + virtual PlaybackManager *createPlaybackManager() const = 0; +}; + +/** + * @brief Dummy session representing this device. + */ +class LocalSession : public ControllableSession { + Q_OBJECT +public: + LocalSession(ApiClient &apiClient, QObject *parent = nullptr); + QString id() const override; + QString name() const override; + QString appName() const override; + DeviceType deviceType() const override; + QString userName() const override; + PlaybackManager *createPlaybackManager() const override; +private: + ApiClient &m_apiClient; +}; + +/** + * @brief A session on the Jellyfin server that can be controlled. + */ +class ControllableJellyfinSession : public ControllableSession { + Q_OBJECT +public: + ControllableJellyfinSession(QSharedPointer info, QObject *parent = nullptr); + QString id() const override; + QString name() const override; + QString appName() const override; + DeviceType deviceType() const override; + QString userName() const override; + PlaybackManager *createPlaybackManager() const override; +private: + QSharedPointer m_data; +}; + +/** + * Abstract class for finding remotely controllable sessions + */ +class RemoteSessionScanner : public QObject { + Q_OBJECT +public: + explicit RemoteSessionScanner(QObject *parent = nullptr); + /** + * The session scanner should start discovering sessions + */ + virtual void startScanning() = 0; + /** + * The session scanner should stop discovering sessions + */ + virtual void stopScanning() = 0; +signals: + /** + * This signal should be emitted when an session has been discovered. + * The session should be reparented to whoever is listening for this signal. + */ + void sessionFound(Jellyfin::Model::ControllableSession *session); + /** + * Should be emitted when an session is gone. + */ + void sessionLost(const QString &sessionId); + /** + * Should be emitted when the listener should delete all sessions by this discoverer. + */ + void resetSessions(); +}; + + +class RemoteJellyfinSessionScannerPrivate; +/** + * @brief Lists controllable Jellyfin sessions from the Jellyfin server + */ +class RemoteJellyfinSessionScanner : public RemoteSessionScanner { + Q_OBJECT + Q_DECLARE_PRIVATE(RemoteJellyfinSessionScanner); +public: + explicit RemoteJellyfinSessionScanner(ApiClient *client, QObject *parent); + virtual ~RemoteJellyfinSessionScanner(); + + void startScanning() override; + void stopScanning() override; +private: + QScopedPointer d_ptr; +}; + +} // NS Model +} // NS Jellyfin + +#endif // JELLYFIN_MODEL_CONTROLLABLESESSION_H diff --git a/core/include/JellyfinQt/model/playbackmanager.h b/core/include/JellyfinQt/model/playbackmanager.h index 7918554..1b49bbc 100644 --- a/core/include/JellyfinQt/model/playbackmanager.h +++ b/core/include/JellyfinQt/model/playbackmanager.h @@ -62,10 +62,25 @@ class PlaybackManager : public QObject { Q_PROPERTY(bool resumePlayback READ resumePlayback WRITE setResumePlayback NOTIFY resumePlaybackChanged) Q_PROPERTY(int audioIndex READ audioIndex WRITE setAudioIndex NOTIFY audioIndexChanged) Q_PROPERTY(int subtitleIndex READ subtitleIndex WRITE setSubtitleIndex NOTIFY subtitleIndexChanged) + /** + * @brief The position in ticks in the currently playing item + */ Q_PROPERTY(qint64 position READ position NOTIFY positionChanged) + /** + * @brief The duration in ticks of the currently playing item + */ Q_PROPERTY(qint64 duration READ duration NOTIFY durationChanged) + /** + * @brief Whether the playbackmanager is currently able to seek + */ Q_PROPERTY(bool seekable READ seekable NOTIFY seekableChanged) + /** + * @brief Whether the currently playing item has audio + */ Q_PROPERTY(bool hasAudio READ hasAudio NOTIFY hasAudioChanged) + /** + * @brief Whether the currently playing item has video + */ Q_PROPERTY(bool hasVideo READ hasVideo NOTIFY hasVideoChanged) Q_PROPERTY(Jellyfin::Model::PlayerStateClass::Value playbackState READ playbackState NOTIFY playbackStateChanged) Q_PROPERTY(Jellyfin::Model::MediaStatusClass::Value mediaStatus READ mediaStatus NOTIFY mediaStatusChanged) @@ -104,7 +119,15 @@ public: virtual bool hasAudio() const = 0; virtual bool hasVideo() const = 0; + /** + * @brief Start playing the given item + */ virtual void playItem(QSharedPointer item) = 0; + /** + * @brief Set the playlist to the given playlist and start playing the item at the given index + * @param items The list of items to play + * @param index Index of the item to play + */ virtual void playItemInList(const QList> &items, int index) = 0; signals: diff --git a/core/include/JellyfinQt/model/remotejellyfinplayback.h b/core/include/JellyfinQt/model/remotejellyfinplayback.h new file mode 100644 index 0000000..2d123c3 --- /dev/null +++ b/core/include/JellyfinQt/model/remotejellyfinplayback.h @@ -0,0 +1,74 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2023 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_MODEL_REMOTEJELLYFINPLAYBACK_H +#define JELLYFIN_MODEL_REMOTEJELLYFINPLAYBACK_H + +#include +#include + +#include +#include + +namespace Jellyfin { + +class ApiClient; + +namespace Model { + +class RemoteJellyfinPlayback : public PlaybackManager { +public: + RemoteJellyfinPlayback(ApiClient &apiClient, QObject *parent = nullptr); + + + // PlaybackManager + void swap(PlaybackManager &other) override; + PlayerState playbackState() const override; + MediaStatus mediaStatus() const override; + bool hasNext() const override; + bool hasPrevious() const override; + PlaybackManagerError error() const override; + const QString &errorString() const override; + qint64 position() const override; + qint64 duration() const override; + bool seekable() const override; + bool hasAudio() const override; + bool hasVideo() const override; + void playItem(QSharedPointer item) override; + void playItemInList(const QList > &items, int index) override; + +public slots: + void pause() override; + void play() override; + void playItemId(const QString &id) override; + void previous() override; + void next() override; + void goTo(int index) override; + void stop() override; + void seek(qint64 pos) override; +private: + void sendGeneralCommand(DTO::GeneralCommandType command, QJsonObject arguments = QJsonObject()); + ApiClient &m_apiClient; +}; + + +} // NS Model +} // NS Jellyfin + + +#endif // JELLYFIN_MODEL_REMOTEJELLYFINPLAYBACK_H diff --git a/core/include/JellyfinQt/viewmodel/playbackmanager.h b/core/include/JellyfinQt/viewmodel/playbackmanager.h index 3d36907..c956ef9 100644 --- a/core/include/JellyfinQt/viewmodel/playbackmanager.h +++ b/core/include/JellyfinQt/viewmodel/playbackmanager.h @@ -20,11 +20,12 @@ #define JELLYFIN_VIEWMODEL_PLAYBACKMANAGER_H #include +#include #include #include #include -#include #include +#include #include #include #include @@ -34,17 +35,18 @@ #include -#include "../dto/baseitemdto.h" -#include "../dto/playbackinfodto.h" -#include "../dto/playbackinforesponse.h" -#include "../dto/playmethod.h" -#include "../loader/requesttypes.h" -#include "../model/player.h" -#include "../model/playlist.h" -#include "../support/jsonconv.h" -#include "../viewmodel/item.h" -#include "../viewmodel/playlist.h" -#include "../apiclient.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "itemmodel.h" @@ -81,6 +83,13 @@ public: virtual ~PlaybackManager(); Q_PROPERTY(ApiClient *apiClient READ apiClient WRITE setApiClient) + Q_PROPERTY(QString controllingSessionId READ controllingSessionId NOTIFY controllingSessionIdChanged) + Q_PROPERTY(QString controllingSessionName READ controllingSessionName NOTIFY controllingSessionNameChanged) + /** + * Whether the playback is done by this client + */ + Q_PROPERTY(bool controllingSessionLocal READ controllingSessionLocal NOTIFY controllingSessionLocalChanged) + Q_PROPERTY(int audioIndex READ audioIndex WRITE setAudioIndex NOTIFY audioIndexChanged) Q_PROPERTY(int subtitleIndex READ subtitleIndex WRITE setSubtitleIndex NOTIFY subtitleIndexChanged) Q_PROPERTY(QString streamUrl READ streamUrl NOTIFY streamUrlChanged) @@ -91,7 +100,7 @@ public: Q_PROPERTY(bool resumePlayback READ resumePlayback WRITE setResumePlayback NOTIFY resumePlaybackChanged) Q_PROPERTY(Jellyfin::DTO::PlayMethodClass::Value playMethod READ playMethod NOTIFY playMethodChanged) - // Current Item and queue informatoion + // Current Item and queue information Q_PROPERTY(QObject *item READ item NOTIFY itemChanged) Q_PROPERTY(int queueIndex READ queueIndex NOTIFY queueIndexChanged) Q_PROPERTY(Jellyfin::ViewModel::Playlist *queue READ queue NOTIFY queueChanged) @@ -124,6 +133,11 @@ public: ViewModel::Item *item() const; QSharedPointer dataItem() const; + QSharedPointer controllingSession() const; + void setControllingSession(QSharedPointer session); + QString controllingSessionId() const; + QString controllingSessionName() const; + bool controllingSessionLocal() const; QString streamUrl() const; PlayMethod playMethod() const; qint64 position() const; @@ -146,6 +160,10 @@ public: void setHandlePlaystateCommands(bool newHandlePlaystateCommands); signals: void itemChanged(); + void controllingSessionChanged(); + void controllingSessionIdChanged(); + void controllingSessionNameChanged(); + void controllingSessionLocalChanged(); void streamUrlChanged(const QString &newStreamUrl); void autoOpenChanged(bool autoOpen); void audioIndexChanged(int audioIndex); diff --git a/core/include/JellyfinQt/viewmodel/remotedevice.h b/core/include/JellyfinQt/viewmodel/remotedevice.h new file mode 100644 index 0000000..c5183da --- /dev/null +++ b/core/include/JellyfinQt/viewmodel/remotedevice.h @@ -0,0 +1,118 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2022 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_REMOTEDEVICE_H +#define JELLYFIN_VIEWMODEL_REMOTEDEVICE_H + +#include + +#include +#include +#include +#include +#include + +#include + +namespace Jellyfin { + +class ApiClient; + +namespace ViewModel { + +class PlaybackManager; + +/** + * @brief AbstractListModel of remotely controllable devices by JellyfinQt. + * + * This class controls a set of \link ViewModel::RemoteSessionScanner RemoteSessionScanners\endlink and + * puts their found devices in this list. + */ +class RemoteDeviceList : public QAbstractListModel, public QQmlParserStatus { + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + /** + * The ApiClient for interacting with the Jellyfin API. + */ + Q_PROPERTY(Jellyfin::ApiClient* apiClient READ apiClient WRITE setApiClient NOTIFY apiClientChanged) + /** + * Gets/sets whether the model is scanning for other devices. + */ + Q_PROPERTY(bool scanning READ scanning WRITE setScanning NOTIFY scanningChanged); +public: + enum RoleNames { + jellyfinId = Qt::UserRole + 1, + name, + deviceName, + deviceType, + userName, + session + }; + + explicit RemoteDeviceList(QObject *parent = nullptr); + + ApiClient *apiClient() const { return m_apiClient; } + void setApiClient(ApiClient *apiClient); + + bool scanning() const { return m_scanning; } + void setScanning(bool scanning); + + // QAbstractListModel + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override { + return { + { RoleNames::jellyfinId, "jellyfinId"}, + { RoleNames::name, "name" }, + { RoleNames::deviceName, "deviceName" }, + { RoleNames::deviceType, "deviceType" }, + { RoleNames::userName, "userName" }, + { RoleNames::session, "session" } + }; + } + + /** + * @brief Sets the PlaybackManager to control the session at the given index in this model + * @param manager The PlaybackManager + * @param index The index of the session that should be controlled + */ + Q_INVOKABLE void activateSession(Jellyfin::ViewModel::PlaybackManager *manager, int index); + + // QQmlParserStatus + void classBegin() override; + void componentComplete() override; +signals: + void apiClientChanged(); + void scanningChanged(); +private slots: + void onSessionFound(Jellyfin::Model::ControllableSession * session); + void onSessionLost(QString sessionId); + void onSessionsReset(); +private: + ApiClient *m_apiClient = nullptr; + bool m_scanning = false; + bool m_componentComplete = false; + QList>> m_sessions; + QList m_scanners; +}; + +} // NS ViewModel +} // NS Jellyfin + +#endif // JELLYFIN_VIEWMODEL_REMOTEDEVICE_H diff --git a/core/include/JellyfinQt/viewmodel/usermodel.h b/core/include/JellyfinQt/viewmodel/usermodel.h index b49625a..1ea026a 100644 --- a/core/include/JellyfinQt/viewmodel/usermodel.h +++ b/core/include/JellyfinQt/viewmodel/usermodel.h @@ -43,17 +43,17 @@ public: userId = Qt::UserRole + 1, name, hasPassword, - primaryImageTag, + primaryImageTag }; explicit UserModel (QObject *parent = nullptr); virtual QHash roleNames() const override { return { - { RoleNames::userId, "userId" }, - { RoleNames::name, "name" }, - { RoleNames::hasPassword, "hasPassword" }, - { RoleNames::primaryImageTag, "primaryImageTag" }, + { RoleNames::userId, "userId" }, + { RoleNames::name, "name" }, + { RoleNames::hasPassword, "hasPassword" }, + { RoleNames::primaryImageTag, "primaryImageTag" } }; } QVariant data(const QModelIndex &index, int role) const override; diff --git a/core/src/apiclient.cpp b/core/src/apiclient.cpp index db6a33e..b607a0a 100644 --- a/core/src/apiclient.cpp +++ b/core/src/apiclient.cpp @@ -44,6 +44,7 @@ public: // Authentication-related variables QString token; QString baseUrl; + QString appName; QString deviceName; QString deviceId; QString userId; @@ -103,6 +104,21 @@ void ApiClient::setBaseUrl(const QString &url) { emit this->baseUrlChanged(d->baseUrl); } +const QString &ApiClient::appName() const { + const Q_D(ApiClient); + return d->appName; +} + +void ApiClient::setAppName(const QString &name) { + Q_D(ApiClient); + d->appName = name; + emit appNameChanged(name); + + if (!d->componentBeingParsed) { + generateDeviceProfile(); + } +} + const QString &ApiClient::userId() const { Q_D(const ApiClient); return d->userId; @@ -220,7 +236,7 @@ void ApiClient::addBaseRequestHeaders(QNetworkRequest &request, const QString &p void ApiClient::addTokenHeader(QNetworkRequest &request) const { Q_D(const ApiClient); QString authentication = "MediaBrowser "; - authentication += "Client=\"Sailfin\""; + authentication += "Client=\"" +d->appName +"\""; authentication += ", Device=\"" + d->deviceName + "\""; authentication += ", DeviceId=\"" + d->deviceId + "\""; authentication += ", Version=\"" + version() + "\""; @@ -425,7 +441,6 @@ QString ApiClient::downloadUrl(const QString &itemId) const { void ApiClient::generateDeviceProfile() { Q_D(ApiClient); QSharedPointer deviceProfile = QSharedPointer::create(Model::DeviceProfile::generateProfile()); - deviceProfile->setName(d->deviceName); deviceProfile->setJellyfinId(d->deviceId); deviceProfile->setFriendlyName(QSysInfo::prettyProductName()); deviceProfile->setMaxStreamingBitrate(d->settings->maxStreamingBitRate()); diff --git a/core/src/jellyfin.cpp b/core/src/jellyfin.cpp index 843a62c..f637bce 100644 --- a/core/src/jellyfin.cpp +++ b/core/src/jellyfin.cpp @@ -30,6 +30,7 @@ #include "JellyfinQt/eventbus.h" #include "JellyfinQt/serverdiscoverymodel.h" #include "JellyfinQt/websocket.h" +#include "JellyfinQt/model/controllablesession.h" #include "JellyfinQt/model/player.h" #include "JellyfinQt/viewmodel/item.h" #include "JellyfinQt/viewmodel/itemmodel.h" @@ -39,6 +40,7 @@ #include "JellyfinQt/viewmodel/platformmediacontrol.h" #include "JellyfinQt/viewmodel/playbackmanager.h" #include "JellyfinQt/viewmodel/playlist.h" +#include "JellyfinQt/viewmodel/remotedevice.h" #include "JellyfinQt/viewmodel/settings.h" #include "JellyfinQt/viewmodel/userdata.h" #include "JellyfinQt/viewmodel/usermodel.h" @@ -66,6 +68,7 @@ void JellyfinPlugin::registerTypes(const char *uri) { qmlRegisterType(uri, 1, 0, "ItemModel"); qmlRegisterType(uri, 1, 0, "UserModel"); qmlRegisterUncreatableType(uri, 1, 0, "Playlist", "Available via PlaybackManager"); + qmlRegisterType(uri, 1, 0, "RemoteDeviceList"); // Loaders qmlRegisterUncreatableType(uri, 1, 0, "LoaderBase", "Use one of its subclasses"); @@ -90,6 +93,7 @@ void JellyfinPlugin::registerTypes(const char *uri) { qmlRegisterUncreatableType(uri, 1, 0, "NowPlayingSection", "Is an enum"); qmlRegisterUncreatableType(uri, 1, 0, "PlayerState", "Is an enum"); qmlRegisterUncreatableType(uri, 1, 0, "MediaStatus", "Is an enum"); + qmlRegisterUncreatableType(uri, 1, 0, "DeviceType", "Is an enum"); qRegisterMetaType(); } diff --git a/core/src/model/controllablesession.cpp b/core/src/model/controllablesession.cpp new file mode 100644 index 0000000..1bec6a0 --- /dev/null +++ b/core/src/model/controllablesession.cpp @@ -0,0 +1,132 @@ +#include "JellyfinQt/model/controllablesession.h" + +#include + +#include "JellyfinQt/loader/http/session.h" +#include "JellyfinQt/loader/requesttypes.h" +#include + + +namespace Jellyfin { +namespace Model { + +ControllableSession::ControllableSession(QObject *parent) + : QObject(parent) {} + +// LocalSession +LocalSession::LocalSession(ApiClient &apiClient, QObject *parent) + : ControllableSession(parent), m_apiClient(apiClient) {} + +QString LocalSession::id() const { + return m_apiClient.deviceId(); +} + +QString LocalSession::appName() const { + return m_apiClient.appName(); +} + +QString LocalSession::name() const { + //: Shown in a list of devices to indicate that media should be played on this device + return tr("This device"); +} + +DeviceType LocalSession::deviceType() const { + return DeviceType::Unknown; +} + +QString LocalSession::userName() const { + return m_apiClient.userId(); +} + +PlaybackManager *LocalSession::createPlaybackManager() const { + return new LocalPlaybackManager(); +} + +// ControllableJellyfinSession +ControllableJellyfinSession::ControllableJellyfinSession(const QSharedPointer info, QObject *parent) + : ControllableSession(parent), + m_data(info) {} + +QString ControllableJellyfinSession::id() const { + return m_data->jellyfinId(); +} + +QString ControllableJellyfinSession::appName() const { + return m_data->client(); +} + +QString ControllableJellyfinSession::name() const { + return m_data->deviceName(); +} + +DeviceType ControllableJellyfinSession::deviceType() const { + return DeviceType::Unknown; +} + +QString ControllableJellyfinSession::userName() const { + return m_data->userName(); +} + +PlaybackManager * ControllableJellyfinSession::createPlaybackManager() const { + // TODO: implement + return nullptr; +} + +RemoteSessionScanner::RemoteSessionScanner(QObject *parent) + : QObject(parent) {} + +using GetSessionsLoader = Loader::HTTP::GetSessionsLoader; +class RemoteJellyfinSessionScannerPrivate { +public: + RemoteJellyfinSessionScannerPrivate(ApiClient *apiClient) + : apiClient(apiClient) { + }; + + ApiClient *apiClient; + GetSessionsLoader *loader = nullptr; +}; + + +RemoteJellyfinSessionScanner::RemoteJellyfinSessionScanner(ApiClient *apiClient, QObject *parent) + : RemoteSessionScanner(parent), + d_ptr(new RemoteJellyfinSessionScannerPrivate(apiClient)) { +} + +RemoteJellyfinSessionScanner::~RemoteJellyfinSessionScanner() {} + +void RemoteJellyfinSessionScanner::startScanning() { + Q_D(RemoteJellyfinSessionScanner); + if (d->loader != nullptr) return; + + emit resetSessions(); + emit sessionFound(new LocalSession(*d->apiClient)); + + Loader::GetSessionsParams params; + params.setControllableByUserId(d->apiClient->userId()); + d->loader = new GetSessionsLoader(d->apiClient); + d->loader->setParameters(params); + connect(d->loader, &Loader::HTTP::GetSessionsLoader::ready, this, [this, d]() { + if (d->loader == nullptr) return; + QList sessions = d->loader->result(); + + for(auto it = sessions.begin(); it != sessions.end(); it++) { + + // Skip this device + if (it->jellyfinId() == d->apiClient->deviceId()) continue; + + emit sessionFound(new ControllableJellyfinSession(QSharedPointer::create(*it))); + } + }); + d->loader->load(); +} + +void RemoteJellyfinSessionScanner::stopScanning() { + Q_D(RemoteJellyfinSessionScanner); + if (d->loader != nullptr) { + d->loader->deleteLater(); + d->loader = nullptr; + } +} + +} // NS Model +} // NS Jellyfin diff --git a/core/src/model/player.cpp b/core/src/model/player.cpp index e1213d7..62e3a4a 100644 --- a/core/src/model/player.cpp +++ b/core/src/model/player.cpp @@ -89,7 +89,7 @@ QtMultimediaPlayerPrivate::QtMultimediaPlayerPrivate(QtMultimediaPlayer *q) q->connect(m_mediaPlayer, &QMediaPlayer::seekableChanged, q, &QtMultimediaPlayer::seekableChanged); q->connect(m_mediaPlayer, &QMediaPlayer::audioAvailableChanged, q, &QtMultimediaPlayer::hasAudioChanged); q->connect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, q, &QtMultimediaPlayer::hasVideoChanged); - q->connect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::Error)), q, SLOT(errorStringChanged)); + //q->connect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::Error)), q, SLOT(errorStringChanged(QString))); if (m_mediaStreamsControl != nullptr) { q->connect(m_mediaStreamsControl, &QMediaStreamsControl::streamsChanged, q, [this](){ qCDebug(player) << m_mediaStreamsControl->streamCount() << " streams in the medi source"; diff --git a/core/src/model/remotejellyfinplayback.cpp b/core/src/model/remotejellyfinplayback.cpp new file mode 100644 index 0000000..68bc84b --- /dev/null +++ b/core/src/model/remotejellyfinplayback.cpp @@ -0,0 +1,132 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2023 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 + +#include +#include +#include + +namespace Jellyfin { +namespace Model { + +RemoteJellyfinPlayback::RemoteJellyfinPlayback(ApiClient &apiClient, QObject *parent) + : PlaybackManager(parent), m_apiClient(apiClient) { + +} + +void RemoteJellyfinPlayback::swap(PlaybackManager &other) { + +} + +PlayerState RemoteJellyfinPlayback::playbackState() const { + +} + +MediaStatus RemoteJellyfinPlayback::mediaStatus() const { + +} + +bool RemoteJellyfinPlayback::hasNext() const { + +} + +bool RemoteJellyfinPlayback::hasPrevious() const { + +} + +PlaybackManagerError RemoteJellyfinPlayback::error() const { + +} + +const QString &RemoteJellyfinPlayback::errorString() const { + +} + +qint64 RemoteJellyfinPlayback::position() const { + +} + +qint64 RemoteJellyfinPlayback::duration() const { + +} + +bool RemoteJellyfinPlayback::seekable() const { + +} + +bool RemoteJellyfinPlayback::hasAudio() const { + +} + +bool RemoteJellyfinPlayback::hasVideo() const { + +} + +void RemoteJellyfinPlayback::playItem(QSharedPointer item) { + +} + +void RemoteJellyfinPlayback::playItemInList(const QList > &items, int index) { + +} + +void RemoteJellyfinPlayback::pause() { +} + +void RemoteJellyfinPlayback::play() { + +} + +void RemoteJellyfinPlayback::playItemId(const QString &id) { + +} + +void RemoteJellyfinPlayback::previous() { + +} + +void RemoteJellyfinPlayback::next() { + +} + +void RemoteJellyfinPlayback::goTo(int index) { + +} + +void RemoteJellyfinPlayback::stop() { + +} + +void RemoteJellyfinPlayback::seek(qint64 pos) { + +} + +void RemoteJellyfinPlayback::sendGeneralCommand(DTO::GeneralCommandType command, QJsonObject arguments) { + Loader::SendFullGeneralCommandParams params; + QSharedPointer fullCommand = QSharedPointer::create(command, m_apiClient.userId()); + fullCommand->setArguments(arguments); + // FIXME: send command +} + + + +} // NS Model +} // NS Jellyfin + diff --git a/core/src/viewmodel/playbackmanager.cpp b/core/src/viewmodel/playbackmanager.cpp index c296534..8ca1496 100644 --- a/core/src/viewmodel/playbackmanager.cpp +++ b/core/src/viewmodel/playbackmanager.cpp @@ -23,10 +23,13 @@ #include #include -// #include "JellyfinQt/DTO/dto.h" #include +#include #include #include + +#include + #include namespace Jellyfin { @@ -47,7 +50,8 @@ public: PlaybackManager *q_ptr = nullptr; ApiClient *m_apiClient = nullptr; - Model::PlaybackManager *m_impl = nullptr; + QSharedPointer m_session; + QScopedPointer m_impl; /// The currently played item that will be shown in the GUI ViewModel::Item *m_displayItem = nullptr; @@ -59,6 +63,7 @@ public: PlaybackManagerPrivate::PlaybackManagerPrivate(PlaybackManager *q) : q_ptr(q), + m_session(nullptr), m_impl(new Model::LocalPlaybackManager(q)), m_displayItem(new ViewModel::Item(q)), m_displayQueue(new ViewModel::Playlist(m_impl->queue())) { @@ -73,21 +78,22 @@ PlaybackManager::PlaybackManager(QObject *parent) Q_D(PlaybackManager); // Set up connections. - connect(d->m_impl, &Model::PlaybackManager::positionChanged, this, &PlaybackManager::positionChanged); - connect(d->m_impl, &Model::PlaybackManager::durationChanged, this, &PlaybackManager::durationChanged); - connect(d->m_impl, &Model::PlaybackManager::hasNextChanged, this, &PlaybackManager::hasNextChanged); - connect(d->m_impl, &Model::PlaybackManager::hasPreviousChanged, this, &PlaybackManager::hasPreviousChanged); - connect(d->m_impl, &Model::PlaybackManager::seekableChanged, this, &PlaybackManager::seekableChanged); - connect(d->m_impl, &Model::PlaybackManager::queueIndexChanged, this, &PlaybackManager::queueIndexChanged); - connect(d->m_impl, &Model::PlaybackManager::itemChanged, this, &PlaybackManager::mediaPlayerItemChanged); - connect(d->m_impl, &Model::PlaybackManager::playbackStateChanged, this, &PlaybackManager::playbackStateChanged); - if (auto localImp = qobject_cast(d->m_impl)) { + connect(d->m_impl.data(), &Model::PlaybackManager::positionChanged, this, &PlaybackManager::positionChanged); + connect(d->m_impl.data(), &Model::PlaybackManager::durationChanged, this, &PlaybackManager::durationChanged); + connect(d->m_impl.data(), &Model::PlaybackManager::hasNextChanged, this, &PlaybackManager::hasNextChanged); + connect(d->m_impl.data(), &Model::PlaybackManager::hasPreviousChanged, this, &PlaybackManager::hasPreviousChanged); + connect(d->m_impl.data(), &Model::PlaybackManager::seekableChanged, this, &PlaybackManager::seekableChanged); + connect(d->m_impl.data(), &Model::PlaybackManager::queueIndexChanged, this, &PlaybackManager::queueIndexChanged); + connect(d->m_impl.data(), &Model::PlaybackManager::itemChanged, this, &PlaybackManager::mediaPlayerItemChanged); + connect(d->m_impl.data(), &Model::PlaybackManager::playbackStateChanged, this, &PlaybackManager::playbackStateChanged); + + if (auto localImp = qobject_cast(d->m_impl.data())) { connect(localImp, &Model::LocalPlaybackManager::streamUrlChanged, this, [this](const QUrl& newUrl){ - this->streamUrlChanged(newUrl.toString()); + emit this->streamUrlChanged(newUrl.toString()); }); connect(localImp, &Model::LocalPlaybackManager::playMethodChanged, this, &PlaybackManager::playMethodChanged); } - connect(d->m_impl, &Model::PlaybackManager::mediaStatusChanged, this, &PlaybackManager::mediaStatusChanged); + connect(d->m_impl.data(), &Model::PlaybackManager::mediaStatusChanged, this, &PlaybackManager::mediaStatusChanged); } PlaybackManager::~PlaybackManager() { @@ -107,6 +113,10 @@ void PlaybackManager::setApiClient(ApiClient *apiClient) { d->m_impl->setApiClient(apiClient); if (d->m_apiClient != nullptr) { + // Set the session to a new LocalSession in case it hasn't been set yet. + if (d->m_session.isNull()) { + setControllingSession(QSharedPointer::create(*apiClient, this)); + } connect(d->m_apiClient->eventbus(), &EventBus::playstateCommandReceived, this, &PlaybackManager::handlePlaystateRequest); } } @@ -155,9 +165,42 @@ ApiClient * PlaybackManager::apiClient() const { return d->m_apiClient; } +QSharedPointer PlaybackManager::controllingSession() const { + const Q_D(PlaybackManager); + return d->m_session; +} + +void PlaybackManager::setControllingSession(QSharedPointer session) { + Q_D(PlaybackManager); + + qCDebug(playbackManager()) << "Now controlling session " << session->name(); + session->setParent(this); + d->m_session.swap(session); + // TODO: swap out playback manager + emit controllingSessionChanged(); + emit controllingSessionIdChanged(); + emit controllingSessionNameChanged(); + emit controllingSessionLocalChanged(); +} + +QString PlaybackManager::controllingSessionId() const { + const Q_D(PlaybackManager); + return d->m_session->id(); +} + +QString PlaybackManager::controllingSessionName() const { + const Q_D(PlaybackManager); + return d->m_session->name(); +} + +bool PlaybackManager::controllingSessionLocal() const { + const Q_D(PlaybackManager); + return qobject_cast(d->m_impl.data()) != nullptr; +} + QString PlaybackManager::streamUrl() const { const Q_D(PlaybackManager); - if (Model::LocalPlaybackManager *lpm = qobject_cast(d->m_impl)) { + if (Model::LocalPlaybackManager *lpm = qobject_cast(d->m_impl.data())) { return lpm->streamUrl().toString(); } else { return QStringLiteral(""); @@ -166,7 +209,7 @@ QString PlaybackManager::streamUrl() const { PlayMethod PlaybackManager::playMethod() const { const Q_D(PlaybackManager); - if (Model::LocalPlaybackManager *lpm = qobject_cast(d->m_impl)) { + if (Model::LocalPlaybackManager *lpm = qobject_cast(d->m_impl.data())) { return lpm->playMethod(); } else { return PlayMethod::EnumNotSet; @@ -210,7 +253,7 @@ bool PlaybackManager::hasPrevious() const { QObject* PlaybackManager::mediaObject() const { const Q_D(PlaybackManager); - if (auto localPb = qobject_cast(d->m_impl)) { + if (auto localPb = qobject_cast(d->m_impl.data())) { return localPb->player()->videoOutputSource(); } else { return nullptr; diff --git a/core/src/viewmodel/remotedevice.cpp b/core/src/viewmodel/remotedevice.cpp new file mode 100644 index 0000000..25236f9 --- /dev/null +++ b/core/src/viewmodel/remotedevice.cpp @@ -0,0 +1,152 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2022 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 + +#include +#include + +namespace Jellyfin { +namespace ViewModel { + +RemoteDeviceList::RemoteDeviceList(QObject *parent) + : QAbstractListModel(parent) {} + +void RemoteDeviceList::classBegin() {} +void RemoteDeviceList::componentComplete() { + m_componentComplete = true; + if (m_apiClient != nullptr) { + setApiClient(m_apiClient); + } +} + +void RemoteDeviceList::setApiClient(ApiClient *apiClient) { + if (m_apiClient != nullptr) { + for (auto it = m_scanners.begin(); it != m_scanners.end(); it++) { + disconnect(*it, &Model::RemoteSessionScanner::sessionFound, this, &RemoteDeviceList::onSessionFound); + disconnect(*it, &Model::RemoteSessionScanner::sessionLost, this, &RemoteDeviceList::onSessionLost); + disconnect(*it, &Model::RemoteSessionScanner::resetSessions, this, &RemoteDeviceList::onSessionsReset); + } + for (auto it = m_sessions.begin(); it != m_sessions.end(); it++) { + it->first->stopScanning(); + it->first->deleteLater(); + it->second->deleteLater(); + } + m_scanners.clear(); + m_sessions.clear(); + } + m_apiClient = apiClient; + emit apiClientChanged(); + if (!m_componentComplete) return; + + m_scanners.append(new Model::RemoteJellyfinSessionScanner(m_apiClient, this)); + for (auto it = m_scanners.begin(); it != m_scanners.end(); it++) { + connect(*it, &Model::RemoteSessionScanner::sessionFound, this, &RemoteDeviceList::onSessionFound); + connect(*it, &Model::RemoteSessionScanner::sessionLost, this, &RemoteDeviceList::onSessionLost); + connect(*it, &Model::RemoteSessionScanner::resetSessions, this, &RemoteDeviceList::onSessionsReset); + } +} + +int RemoteDeviceList::rowCount(const QModelIndex &parent) const { + return m_sessions.size(); +} + +QVariant RemoteDeviceList::data(const QModelIndex &index, int role) const { + int row = index.row(); + if (!index.isValid() || row < 0 || row > rowCount()) return QVariant(); + const QSharedPointer session = m_sessions.at(row).second; + if (session.isNull()) return QVariant(); + + switch (role) { + case RoleNames::jellyfinId: + return session->id(); + case RoleNames::name: + return session->name(); + case RoleNames::deviceName: + return session->appName(); + case RoleNames::deviceType: + return QVariant::fromValue(session->deviceType()); + case RoleNames::userName: + return session->userName(); + default: + return QVariant(); + } +} + +void RemoteDeviceList::activateSession(PlaybackManager *manager, int index) { + manager->setControllingSession(m_sessions.at(index).second); +} + +void RemoteDeviceList::setScanning(bool scanning) { + if (scanning == m_scanning) return; + m_scanning = scanning; + emit scanningChanged(); + + if (scanning) { + for (auto it = m_scanners.begin(); it != m_scanners.end(); it++) { + (*it)->startScanning(); + } + } else { + for (auto it = m_scanners.begin(); it != m_scanners.end(); it++) { + (*it)->stopScanning(); + } + } +} + +void RemoteDeviceList::onSessionFound(Model::ControllableSession *session) { + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + m_sessions.append(std::make_pair(qobject_cast(sender()), QSharedPointer(session))); + endInsertRows(); +} + +void RemoteDeviceList::onSessionLost(QString sessionId) { + Model::RemoteSessionScanner *scanner = qobject_cast(sender()); + for (int i = 0; i < m_sessions.size(); i++) { + auto row = m_sessions.at(i); + if (row.first == scanner && row.second->name() == sessionId) { + beginRemoveRows(QModelIndex(), i, i); + m_sessions.removeAt(i); + if (row.second->parent() == this) { + row.second->deleteLater(); + } + endRemoveRows(); + i--; + } + } +} + +void RemoteDeviceList::onSessionsReset() { + Model::RemoteSessionScanner *scanner = qobject_cast(sender()); + for (int i = 0; i < m_sessions.size(); i++) { + auto row = m_sessions.at(i); + if (row.first == scanner) { + beginRemoveRows(QModelIndex(), i, i); + m_sessions.removeAt(i); + if (row.second->parent() == this) { + row.second->deleteLater(); + } + endRemoveRows(); + i--; + } + } +} + + +} // NS Model +} // NS Jellyfin diff --git a/sailfish/CMakeLists.txt b/sailfish/CMakeLists.txt index 5018548..8cf33fd 100644 --- a/sailfish/CMakeLists.txt +++ b/sailfish/CMakeLists.txt @@ -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 diff --git a/sailfish/qml/components/PlaybackBar.qml b/sailfish/qml/components/PlaybackBar.qml index 86bcaec..5614521 100644 --- a/sailfish/qml/components/PlaybackBar.qml +++ b/sailfish/qml/components/PlaybackBar.qml @@ -267,7 +267,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" diff --git a/sailfish/qml/harbour-sailfin.qml b/sailfish/qml/harbour-sailfin.qml index deeebd7..f968b5b 100644 --- a/sailfish/qml/harbour-sailfin.qml +++ b/sailfish/qml/harbour-sailfin.qml @@ -50,6 +50,7 @@ ApplicationWindow { ApiClient { id: _apiClient objectName: "Test" + appName: "Sailfin" supportedCommands: [GeneralCommandType.Play, GeneralCommandType.DisplayMessage] } diff --git a/sailfish/qml/pages/ControllableDevicesPage.qml b/sailfish/qml/pages/ControllableDevicesPage.qml new file mode 100644 index 0000000..2a40841 --- /dev/null +++ b/sailfish/qml/pages/ControllableDevicesPage.qml @@ -0,0 +1,63 @@ +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 + HighlightImage { + id: deviceIcon + anchors { + left: parent.left + leftMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + height: parent.contentHeight - 2 * Theme.paddingMedium + width: height + source: "image://theme/icon-m-computer" + 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 + 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 + } + } + } + } + +} diff --git a/sailfish/qml/pages/MainPage.qml b/sailfish/qml/pages/MainPage.qml index e894d75..d8019e0 100644 --- a/sailfish/qml/pages/MainPage.qml +++ b/sailfish/qml/pages/MainPage.qml @@ -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")