mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-11-22 09:15:18 +00:00
Moved playback logic to C++-side (and refractoring)
This commit is contained in:
parent
895731ae38
commit
f7bca333c8
|
@ -46,6 +46,7 @@ class JsonSerializable : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
Q_INVOKABLE JsonSerializable(QObject *parent);
|
Q_INVOKABLE JsonSerializable(QObject *parent);
|
||||||
|
virtual ~JsonSerializable();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Sets this objects properties based on obj.
|
* @brief Sets this objects properties based on obj.
|
||||||
|
@ -56,7 +57,7 @@ public:
|
||||||
private:
|
private:
|
||||||
QVariant jsonToVariant(QMetaProperty prop, const QJsonValue &val, const QJsonObject &root);
|
QVariant jsonToVariant(QMetaProperty prop, const QJsonValue &val, const QJsonObject &root);
|
||||||
QJsonValue variantToJson(const QVariant var) const;
|
QJsonValue variantToJson(const QVariant var) const;
|
||||||
QVariant deserializeQobject(const QJsonObject &obj, const QMetaProperty &prop);
|
QVariant deserializeQObject(const QJsonObject &obj, const QMetaProperty &prop);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Sets the first letter of the string to lower case (to make it camelCase).
|
* @brief Sets the first letter of the string to lower case (to make it camelCase).
|
||||||
|
@ -72,6 +73,8 @@ private:
|
||||||
static QString toPascalCase(QString st);
|
static QString toPascalCase(QString st);
|
||||||
|
|
||||||
static const QRegularExpression m_listExpression;
|
static const QRegularExpression m_listExpression;
|
||||||
|
static const QRegularExpression m_hashExpression;
|
||||||
|
static int findTypeIdForProperty(QString type);
|
||||||
/**
|
/**
|
||||||
* @brief Qt is doing weird. I'll keep track of the metatypes myself.
|
* @brief Qt is doing weird. I'll keep track of the metatypes myself.
|
||||||
*/
|
*/
|
||||||
|
@ -107,17 +110,20 @@ public:
|
||||||
Q_PROPERTY(Status status READ status NOTIFY statusChanged STORED false)
|
Q_PROPERTY(Status status READ status NOTIFY statusChanged STORED false)
|
||||||
Q_PROPERTY(QNetworkReply::NetworkError error READ error NOTIFY errorChanged STORED false)
|
Q_PROPERTY(QNetworkReply::NetworkError error READ error NOTIFY errorChanged STORED false)
|
||||||
Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged STORED false)
|
Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged STORED false)
|
||||||
|
Q_PROPERTY(QStringList extraFields MEMBER m_extraFields WRITE setExtraFields NOTIFY extraFieldsChanged STORED false)
|
||||||
|
|
||||||
Status status() const { return m_status; }
|
Status status() const { return m_status; }
|
||||||
QNetworkReply::NetworkError error() const { return m_error; }
|
QNetworkReply::NetworkError error() const { return m_error; }
|
||||||
QString errorString() const { return m_errorString; }
|
QString errorString() const { return m_errorString; }
|
||||||
|
|
||||||
void setApiClient(ApiClient *newApiClient);
|
void setApiClient(ApiClient *newApiClient);
|
||||||
|
void setExtraFields(const QStringList &extraFields);
|
||||||
signals:
|
signals:
|
||||||
void statusChanged(Status newStatus);
|
void statusChanged(Status newStatus);
|
||||||
void apiClientChanged(ApiClient *newApiClient);
|
void apiClientChanged(ApiClient *newApiClient);
|
||||||
void errorChanged(QNetworkReply::NetworkError newError);
|
void errorChanged(QNetworkReply::NetworkError newError);
|
||||||
void errorStringChanged(QString newErrorString);
|
void errorStringChanged(QString newErrorString);
|
||||||
|
void extraFieldsChanged(const QStringList &newExtraFields);
|
||||||
/**
|
/**
|
||||||
* @brief Convenience signal for status == RemoteData.Ready.
|
* @brief Convenience signal for status == RemoteData.Ready.
|
||||||
*/
|
*/
|
||||||
|
@ -159,6 +165,7 @@ private:
|
||||||
Status m_status = Uninitialised;
|
Status m_status = Uninitialised;
|
||||||
QNetworkReply::NetworkError m_error = QNetworkReply::NoError;
|
QNetworkReply::NetworkError m_error = QNetworkReply::NoError;
|
||||||
QString m_errorString;
|
QString m_errorString;
|
||||||
|
QStringList m_extraFields;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // NS DTO
|
} // NS DTO
|
||||||
|
|
|
@ -93,11 +93,13 @@ public:
|
||||||
Q_PROPERTY(int productionYear READ productionYear WRITE setProductionYear NOTIFY productionYearChanged)
|
Q_PROPERTY(int productionYear READ productionYear WRITE setProductionYear NOTIFY productionYearChanged)
|
||||||
Q_PROPERTY(int indexNumber READ indexNumber WRITE setIndexNumber NOTIFY indexNumberChanged)
|
Q_PROPERTY(int indexNumber READ indexNumber WRITE setIndexNumber NOTIFY indexNumberChanged)
|
||||||
Q_PROPERTY(int indexNumberEnd READ indexNumberEnd WRITE setIndexNumberEnd NOTIFY indexNumberEndChanged)
|
Q_PROPERTY(int indexNumberEnd READ indexNumberEnd WRITE setIndexNumberEnd NOTIFY indexNumberEndChanged)
|
||||||
|
Q_PROPERTY(int parentIndexNumber READ parentIndexNumber WRITE setParentIndexNumber NOTIFY parentIndexNumberChanged)
|
||||||
Q_PROPERTY(bool isFolder READ isFolder WRITE setIsFolder NOTIFY isFolderChanged)
|
Q_PROPERTY(bool isFolder READ isFolder WRITE setIsFolder NOTIFY isFolderChanged)
|
||||||
|
Q_PROPERTY(QString parentID MEMBER m_parentId READ parentId NOTIFY parentIdChanged)
|
||||||
Q_PROPERTY(QString type MEMBER m_type NOTIFY typeChanged)
|
Q_PROPERTY(QString type MEMBER m_type NOTIFY typeChanged)
|
||||||
Q_PROPERTY(QString parentBackdropItemId MEMBER m_parentBackdropItemId NOTIFY parentBackdropItemIdChanged)
|
Q_PROPERTY(QString parentBackdropItemId MEMBER m_parentBackdropItemId NOTIFY parentBackdropItemIdChanged)
|
||||||
Q_PROPERTY(QStringList parentBackdropImageTags MEMBER m_parentBackdropImageTags NOTIFY parentBackdropImageTagsChanged)
|
Q_PROPERTY(QStringList parentBackdropImageTags MEMBER m_parentBackdropImageTags NOTIFY parentBackdropImageTagsChanged)
|
||||||
Q_PROPERTY(UserData *userData MEMBER m_userData NOTIFY userDataChanged)
|
Q_PROPERTY(UserData *userData READ userData WRITE setUserData NOTIFY userDataChanged)
|
||||||
Q_PROPERTY(int recursiveItemCount READ recursiveItemCount WRITE setRecursiveItemCount NOTIFY recursiveItemCountChanged)
|
Q_PROPERTY(int recursiveItemCount READ recursiveItemCount WRITE setRecursiveItemCount NOTIFY recursiveItemCountChanged)
|
||||||
Q_PROPERTY(int childCount READ childCount WRITE setChildCount NOTIFY childCountChanged)
|
Q_PROPERTY(int childCount READ childCount WRITE setChildCount NOTIFY childCountChanged)
|
||||||
Q_PROPERTY(QString albumArtist MEMBER m_albumArtist NOTIFY albumArtistChanged)
|
Q_PROPERTY(QString albumArtist MEMBER m_albumArtist NOTIFY albumArtistChanged)
|
||||||
|
@ -147,8 +149,19 @@ public:
|
||||||
void setIndexNumber(int newIndexNumber) { m_indexNumber = std::optional<int>(newIndexNumber); emit indexNumberChanged(newIndexNumber); }
|
void setIndexNumber(int newIndexNumber) { m_indexNumber = std::optional<int>(newIndexNumber); emit indexNumberChanged(newIndexNumber); }
|
||||||
int indexNumberEnd() const { return m_indexNumberEnd.value_or(-1); }
|
int indexNumberEnd() const { return m_indexNumberEnd.value_or(-1); }
|
||||||
void setIndexNumberEnd(int newIndexNumberEnd) { m_indexNumberEnd = std::optional<int>(newIndexNumberEnd); emit indexNumberEndChanged(newIndexNumberEnd); }
|
void setIndexNumberEnd(int newIndexNumberEnd) { m_indexNumberEnd = std::optional<int>(newIndexNumberEnd); emit indexNumberEndChanged(newIndexNumberEnd); }
|
||||||
|
int parentIndexNumber() const { return m_parentIndexNumber.value_or(-1); }
|
||||||
|
void setParentIndexNumber(int newParentIndexNumber) { m_parentIndexNumber= std::optional<int>(newParentIndexNumber); emit parentIndexNumberChanged(newParentIndexNumber); }
|
||||||
bool isFolder() const { return m_isFolder.value_or(false); }
|
bool isFolder() const { return m_isFolder.value_or(false); }
|
||||||
void setIsFolder(bool newIsFolder) { m_isFolder = newIsFolder; emit isFolderChanged(newIsFolder); }
|
void setIsFolder(bool newIsFolder) { m_isFolder = newIsFolder; emit isFolderChanged(newIsFolder); }
|
||||||
|
QString parentId() const { return m_parentId; }
|
||||||
|
UserData *userData() const { return m_userData; }
|
||||||
|
void setUserData(UserData *newUserData) {
|
||||||
|
if (m_userData != nullptr) {
|
||||||
|
m_userData->deleteLater();
|
||||||
|
}
|
||||||
|
m_userData = newUserData;
|
||||||
|
emit userDataChanged(newUserData);
|
||||||
|
}
|
||||||
int recursiveItemCount() const { return m_recursiveItemCount.value_or(-1); }
|
int recursiveItemCount() const { return m_recursiveItemCount.value_or(-1); }
|
||||||
void setRecursiveItemCount(int newRecursiveItemCount) { m_recursiveItemCount = newRecursiveItemCount; emit recursiveItemCountChanged(newRecursiveItemCount); }
|
void setRecursiveItemCount(int newRecursiveItemCount) { m_recursiveItemCount = newRecursiveItemCount; emit recursiveItemCountChanged(newRecursiveItemCount); }
|
||||||
int childCount() const { return m_childCount.value_or(-1); }
|
int childCount() const { return m_childCount.value_or(-1); }
|
||||||
|
@ -193,6 +206,7 @@ signals:
|
||||||
void indexNumberChanged(int newIndexNumber);
|
void indexNumberChanged(int newIndexNumber);
|
||||||
void indexNumberEndChanged(int newIndexNumberEnd);
|
void indexNumberEndChanged(int newIndexNumberEnd);
|
||||||
void isFolderChanged(bool newIsFolder);
|
void isFolderChanged(bool newIsFolder);
|
||||||
|
void parentIdChanged(const QString &newParentId);
|
||||||
void typeChanged(const QString &newType);
|
void typeChanged(const QString &newType);
|
||||||
void parentBackdropItemIdChanged();
|
void parentBackdropItemIdChanged();
|
||||||
void parentBackdropImageTagsChanged();
|
void parentBackdropImageTagsChanged();
|
||||||
|
@ -213,7 +227,7 @@ signals:
|
||||||
void heightChanged(int newHeight);
|
void heightChanged(int newHeight);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData);
|
void onUserDataChanged(const QString &itemId, UserData *userData);
|
||||||
protected:
|
protected:
|
||||||
// Overrides
|
// Overrides
|
||||||
QString getDataUrl() const override;
|
QString getDataUrl() const override;
|
||||||
|
@ -251,7 +265,9 @@ protected:
|
||||||
std::optional<int> m_productionYear = std::nullopt;
|
std::optional<int> m_productionYear = std::nullopt;
|
||||||
std::optional<int> m_indexNumber = std::nullopt;
|
std::optional<int> m_indexNumber = std::nullopt;
|
||||||
std::optional<int> m_indexNumberEnd = std::nullopt;
|
std::optional<int> m_indexNumberEnd = std::nullopt;
|
||||||
|
std::optional<int> m_parentIndexNumber = std::nullopt;
|
||||||
std::optional<bool> m_isFolder = std::nullopt;
|
std::optional<bool> m_isFolder = std::nullopt;
|
||||||
|
QString m_parentId;
|
||||||
QString m_type;
|
QString m_type;
|
||||||
QString m_parentBackdropItemId;
|
QString m_parentBackdropItemId;
|
||||||
QStringList m_parentBackdropImageTags;
|
QStringList m_parentBackdropImageTags;
|
||||||
|
|
|
@ -35,7 +35,6 @@ public:
|
||||||
Q_INVOKABLE explicit MediaStream(QObject *parent = nullptr);
|
Q_INVOKABLE explicit MediaStream(QObject *parent = nullptr);
|
||||||
MediaStream(const MediaStream &other);
|
MediaStream(const MediaStream &other);
|
||||||
bool operator==(const MediaStream &other);
|
bool operator==(const MediaStream &other);
|
||||||
virtual ~MediaStream() { qDebug() << "MediaStream destroyed"; }
|
|
||||||
|
|
||||||
enum MediaStreamType {
|
enum MediaStreamType {
|
||||||
Undefined,
|
Undefined,
|
||||||
|
|
|
@ -33,26 +33,38 @@ class User : public RemoteData {
|
||||||
public:
|
public:
|
||||||
Q_INVOKABLE User(QObject *parent = nullptr);
|
Q_INVOKABLE User(QObject *parent = nullptr);
|
||||||
|
|
||||||
Q_PROPERTY(QString userId MEMBER m_userId WRITE setUserId NOTIFY userIdChanged)
|
Q_PROPERTY(QString jellyfinId MEMBER m_jellyfinId WRITE setJellyfinId NOTIFY jellyfinIdChanged)
|
||||||
Q_PROPERTY(QString name MEMBER m_name NOTIFY nameChanged)
|
Q_PROPERTY(QString name MEMBER m_name NOTIFY nameChanged)
|
||||||
Q_PROPERTY(QString primaryImageTag MEMBER m_primaryImageTag NOTIFY primaryImageTagChanged)
|
Q_PROPERTY(QString primaryImageTag MEMBER m_primaryImageTag NOTIFY primaryImageTagChanged)
|
||||||
|
Q_PROPERTY(bool hasPassword MEMBER m_hasPassword NOTIFY hasPasswordChanged)
|
||||||
|
Q_PROPERTY(bool hasConfiguredPassword MEMBER m_hasConfiguredPassword NOTIFY hasConfiguredPasswordChanged)
|
||||||
|
Q_PROPERTY(bool hasConfiguredEasyPassword MEMBER m_hasConfiguredEasyPassword NOTIFY hasConfiguredEasyPasswordChanged)
|
||||||
|
|
||||||
void setUserId(const QString &newUserId) {
|
void setJellyfinId(const QString &newJellyfinId) {
|
||||||
this->m_userId = newUserId;
|
if (m_jellyfinId != newJellyfinId) {
|
||||||
emit userIdChanged(newUserId);
|
this->m_jellyfinId = newJellyfinId;
|
||||||
|
emit jellyfinIdChanged(newJellyfinId);
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
signals:
|
signals:
|
||||||
void userIdChanged(const QString &newUserId);
|
|
||||||
void nameChanged(const QString &newName);
|
void nameChanged(const QString &newName);
|
||||||
|
void jellyfinIdChanged(const QString &newJellyfinId);
|
||||||
void primaryImageTagChanged(const QString &newPrimaryImageTag);
|
void primaryImageTagChanged(const QString &newPrimaryImageTag);
|
||||||
|
void hasPasswordChanged(bool newHasPasswordChanged);
|
||||||
|
void hasConfiguredPasswordChanged(bool newHasConfiguredPasswordChanged);
|
||||||
|
void hasConfiguredEasyPasswordChanged(bool newHasConfiguredEasyPasswordChanged);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QString getDataUrl() const override;
|
QString getDataUrl() const override;
|
||||||
bool canReload() const override;
|
bool canReload() const override;
|
||||||
private:
|
private:
|
||||||
QString m_userId;
|
|
||||||
QString m_name;
|
QString m_name;
|
||||||
|
QString m_jellyfinId;
|
||||||
QString m_primaryImageTag;
|
QString m_primaryImageTag;
|
||||||
|
bool m_hasPassword;
|
||||||
|
bool m_hasConfiguredPassword;
|
||||||
|
bool m_hasConfiguredEasyPassword;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // NS DTO
|
} // NS DTO
|
||||||
|
|
|
@ -67,7 +67,7 @@ signals:
|
||||||
void playedChanged(bool newPlayed);
|
void playedChanged(bool newPlayed);
|
||||||
public slots:
|
public slots:
|
||||||
void updateOnServer();
|
void updateOnServer();
|
||||||
void onUpdated(QSharedPointer<UserData> other);
|
void onUpdated(UserData *other);
|
||||||
private:
|
private:
|
||||||
std::optional<double> m_playedPercentage = std::nullopt;
|
std::optional<double> m_playedPercentage = std::nullopt;
|
||||||
qint64 m_playbackPositionTicks = 0;
|
qint64 m_playbackPositionTicks = 0;
|
||||||
|
|
|
@ -162,8 +162,9 @@ signals:
|
||||||
* @param userData The new user data
|
* @param userData The new user data
|
||||||
*
|
*
|
||||||
* Note: only Jellyfin::UserData should connect to this signal, they will update themselves!
|
* Note: only Jellyfin::UserData should connect to this signal, they will update themselves!
|
||||||
|
* Note: the userData is only valid during this callback, afterwards it is deleted!
|
||||||
*/
|
*/
|
||||||
void userDataChanged(const QString &itemId, QSharedPointer<UserData> userData);
|
void userDataChanged(const QString &itemId, UserData *userData);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
/**
|
/**
|
||||||
|
@ -192,7 +193,7 @@ public slots:
|
||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
void defaultNetworkErrorHandler(QNetworkReply::NetworkError error);
|
void defaultNetworkErrorHandler(QNetworkReply::NetworkError error);
|
||||||
void onUserDataChanged(const QString &itemId, QSharedPointer<UserData> newData);
|
void onUserDataChanged(const QString &itemId, UserData *newData);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -27,6 +27,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QtQml>
|
#include <QtQml>
|
||||||
|
#include <QQmlParserStatus>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
#include "apiclient.h"
|
#include "apiclient.h"
|
||||||
|
@ -35,7 +36,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
|
|
||||||
namespace DTO {
|
namespace DTO {
|
||||||
|
class Item;
|
||||||
class JsonSerializable;
|
class JsonSerializable;
|
||||||
|
class User;
|
||||||
}
|
}
|
||||||
class SortOptions : public QObject {
|
class SortOptions : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -61,6 +64,134 @@ public:
|
||||||
Q_ENUM(SortBy)
|
Q_ENUM(SortBy)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Q_OBJECT does not support template classes. This base class declares the
|
||||||
|
* Q_OBJECT related properties and signals.
|
||||||
|
*/
|
||||||
|
class BaseApiModel : public QAbstractListModel, public QQmlParserStatus {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit BaseApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent = nullptr);
|
||||||
|
enum ModelStatus {
|
||||||
|
Uninitialised,
|
||||||
|
Loading,
|
||||||
|
Ready,
|
||||||
|
Error,
|
||||||
|
LoadingMore
|
||||||
|
};
|
||||||
|
Q_ENUM(ModelStatus)
|
||||||
|
|
||||||
|
enum SortOrder {
|
||||||
|
Unspecified,
|
||||||
|
Ascending,
|
||||||
|
Descending
|
||||||
|
};
|
||||||
|
Q_ENUM(SortOrder)
|
||||||
|
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient NOTIFY apiClientChanged)
|
||||||
|
Q_PROPERTY(ModelStatus status READ status NOTIFY statusChanged)
|
||||||
|
|
||||||
|
// Query properties
|
||||||
|
Q_PROPERTY(int limit MEMBER m_limit NOTIFY limitChanged)
|
||||||
|
Q_PROPERTY(QList<QString> sortBy MEMBER m_sortBy NOTIFY sortByChanged)
|
||||||
|
Q_PROPERTY(QList<QString> fields MEMBER m_fields NOTIFY fieldsChanged)
|
||||||
|
Q_PROPERTY(SortOrder sortOrder MEMBER m_sortOrder NOTIFY sortOrderChanged)
|
||||||
|
|
||||||
|
ModelStatus status() const { return m_status; }
|
||||||
|
void setApiClient(ApiClient *newApiClient);
|
||||||
|
void setLimit(int newLimit);
|
||||||
|
|
||||||
|
// From AbstractListModel, gets implemented in ApiModel<T>
|
||||||
|
virtual int rowCount(const QModelIndex &index) const override = 0;
|
||||||
|
virtual QHash<int, QByteArray> roleNames() const override = 0;
|
||||||
|
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override = 0;
|
||||||
|
virtual bool canFetchMore(const QModelIndex &parent) const override = 0;
|
||||||
|
virtual void fetchMore(const QModelIndex &parent) override = 0;
|
||||||
|
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void ready();
|
||||||
|
void apiClientChanged(ApiClient *newApiClient);
|
||||||
|
void statusChanged(ModelStatus newStatus);
|
||||||
|
void limitChanged(int newLimit);
|
||||||
|
void sortByChanged(QList<QString> newSortOrder);
|
||||||
|
void sortOrderChanged(SortOrder newSortOrder);
|
||||||
|
void fieldsChanged(QList<QString> newFields);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
/**
|
||||||
|
* @brief (Re)loads the data into this model. This might make a network request.
|
||||||
|
*/
|
||||||
|
void reload();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
enum LoadType {
|
||||||
|
RELOAD,
|
||||||
|
LOAD_MORE
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiClient *m_apiClient = nullptr;
|
||||||
|
bool m_isBeingParsed = false;
|
||||||
|
// Per-model specific settings.
|
||||||
|
QString m_path;
|
||||||
|
bool m_hasRecordResponse;
|
||||||
|
bool m_addUserId;
|
||||||
|
bool padding; bool padding2;
|
||||||
|
|
||||||
|
// Query/record controlling properties
|
||||||
|
int m_limit = -1;
|
||||||
|
int m_startIndex = 0;
|
||||||
|
int m_totalRecordCount = 0;
|
||||||
|
const int DEFAULT_LIMIT = 100;
|
||||||
|
|
||||||
|
// Query properties
|
||||||
|
QList<QString> m_fields = {};
|
||||||
|
QList<QString> m_sortBy = {};
|
||||||
|
SortOrder m_sortOrder = Unspecified;
|
||||||
|
|
||||||
|
// State properties.
|
||||||
|
ModelStatus m_status = Uninitialised;
|
||||||
|
|
||||||
|
void setStatus(ModelStatus newStatus) {
|
||||||
|
if (this->m_status != newStatus) {
|
||||||
|
this->m_status = newStatus;
|
||||||
|
emit this->statusChanged(newStatus);
|
||||||
|
if (m_status == Ready) {
|
||||||
|
emit ready();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void load(LoadType loadType);
|
||||||
|
virtual void setModelData(QJsonArray &data) = 0;
|
||||||
|
virtual void appendModelData(QJsonArray &data) = 0;
|
||||||
|
/**
|
||||||
|
* @brief Adds parameters to the query
|
||||||
|
* @param query The query to add parameters to
|
||||||
|
*
|
||||||
|
* This method is intended to be overrided by subclasses. It gets called
|
||||||
|
* before a request is made to the server and can be used to enable
|
||||||
|
* query types specific for a certain model to be available.
|
||||||
|
*
|
||||||
|
* Make sure to call the method in the superclass as well!
|
||||||
|
*/
|
||||||
|
virtual void addQueryParameters(QUrlQuery &query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Replaces placeholders in an URL.
|
||||||
|
* @param path The path in which placeholders should be replaced.
|
||||||
|
*
|
||||||
|
* This method is intended to be overrided by subclasses. It gets called
|
||||||
|
* before a request is made to the server and can be used to enable
|
||||||
|
* query types specific for a certain model to be available.
|
||||||
|
*
|
||||||
|
* Make sure to call the method in the superclass as well!
|
||||||
|
*/
|
||||||
|
virtual void replacePathPlaceholders(QString &path);
|
||||||
|
|
||||||
|
virtual void classBegin() override;
|
||||||
|
virtual void componentComplete() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Abstract model for displaying a REST JSON collection. Role names will be based on the fields encountered in the
|
* @brief Abstract model for displaying a REST JSON collection. Role names will be based on the fields encountered in the
|
||||||
|
@ -86,25 +217,9 @@ public:
|
||||||
* The model will have roleNames for "name" and "id".
|
* The model will have roleNames for "name" and "id".
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
class ApiModel : public QAbstractListModel {
|
template <typename T>
|
||||||
Q_OBJECT
|
class ApiModel : public BaseApiModel {
|
||||||
public:
|
public:
|
||||||
enum ModelStatus {
|
|
||||||
Uninitialised,
|
|
||||||
Loading,
|
|
||||||
Ready,
|
|
||||||
Error,
|
|
||||||
LoadingMore
|
|
||||||
};
|
|
||||||
Q_ENUM(ModelStatus)
|
|
||||||
|
|
||||||
enum SortOrder {
|
|
||||||
Unspecified,
|
|
||||||
Ascending,
|
|
||||||
Descending
|
|
||||||
};
|
|
||||||
Q_ENUM(SortOrder)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Creates a new basemodel
|
* @brief Creates a new basemodel
|
||||||
* @param path The path (relative to the baseUrl of JellyfinApiClient) to make the call to.
|
* @param path The path (relative to the baseUrl of JellyfinApiClient) to make the call to.
|
||||||
|
@ -134,22 +249,6 @@ public:
|
||||||
* responseHasRecords should be true
|
* responseHasRecords should be true
|
||||||
*/
|
*/
|
||||||
explicit ApiModel(QString path, bool responseHasRecords, bool passUserId = false, QObject *parent = nullptr);
|
explicit ApiModel(QString path, bool responseHasRecords, bool passUserId = false, QObject *parent = nullptr);
|
||||||
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient NOTIFY apiClientChanged)
|
|
||||||
Q_PROPERTY(ModelStatus status READ status NOTIFY statusChanged)
|
|
||||||
|
|
||||||
// Query properties
|
|
||||||
Q_PROPERTY(int limit MEMBER m_limit NOTIFY limitChanged)
|
|
||||||
Q_PROPERTY(QString parentId MEMBER m_parentId NOTIFY parentIdChanged)
|
|
||||||
Q_PROPERTY(QList<QString> sortBy MEMBER m_sortBy NOTIFY sortByChanged)
|
|
||||||
Q_PROPERTY(QList<QString> fields MEMBER m_fields NOTIFY fieldsChanged)
|
|
||||||
Q_PROPERTY(QString seasonId MEMBER m_seasonId NOTIFY seasonIdChanged)
|
|
||||||
Q_PROPERTY(QList<QString> imageTypes MEMBER m_imageTypes NOTIFY imageTypesChanged)
|
|
||||||
Q_PROPERTY(QList<QString> includeItemTypes MEMBER m_includeItemTypes NOTIFY includeItemTypesChanged)
|
|
||||||
Q_PROPERTY(bool recursive MEMBER m_recursive)
|
|
||||||
Q_PROPERTY(SortOrder sortOrder MEMBER m_sortOrder NOTIFY sortOrderChanged)
|
|
||||||
|
|
||||||
// Path properties
|
|
||||||
Q_PROPERTY(QString show MEMBER m_show NOTIFY showChanged)
|
|
||||||
|
|
||||||
// Standard QAbstractItemModel overrides
|
// Standard QAbstractItemModel overrides
|
||||||
int rowCount(const QModelIndex &index) const override {
|
int rowCount(const QModelIndex &index) const override {
|
||||||
|
@ -161,12 +260,18 @@ public:
|
||||||
bool canFetchMore(const QModelIndex &parent) const override;
|
bool canFetchMore(const QModelIndex &parent) const override;
|
||||||
void fetchMore(const QModelIndex &parent) override;
|
void fetchMore(const QModelIndex &parent) override;
|
||||||
|
|
||||||
ModelStatus status() const { return m_status; }
|
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
template<typename QEnum>
|
template<typename QEnum>
|
||||||
QString enumToString (const QEnum anEnum) { return QVariant::fromValue(anEnum).toString(); }
|
QString enumToString (const QEnum anEnum) { return QVariant::fromValue(anEnum).toString(); }
|
||||||
|
|
||||||
|
// QList-like API
|
||||||
|
T* at(int index) { return m_array.at(index); }
|
||||||
|
int size() { return rowCount(QModelIndex()); }
|
||||||
|
void insert(int index, T* object);
|
||||||
|
void append(T* object) { insert(size(), object); }
|
||||||
|
void removeAt(int index);
|
||||||
|
void removeOne(T* object);
|
||||||
|
|
||||||
template<typename QEnum>
|
template<typename QEnum>
|
||||||
QString enumListToString (const QList<QEnum> enumList) {
|
QString enumListToString (const QList<QEnum> enumList) {
|
||||||
QString result;
|
QString result;
|
||||||
|
@ -176,74 +281,20 @@ public:
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
signals:
|
|
||||||
void apiClientChanged(ApiClient *newApiClient);
|
|
||||||
void statusChanged(ModelStatus newStatus);
|
|
||||||
void limitChanged(int newLimit);
|
|
||||||
void parentIdChanged(QString newParentId);
|
|
||||||
void sortByChanged(QList<QString> newSortOrder);
|
|
||||||
void sortOrderChanged(SortOrder newSortOrder);
|
|
||||||
void showChanged(QString newShow);
|
|
||||||
void seasonIdChanged(QString newSeasonId);
|
|
||||||
void fieldsChanged(QList<QString> newFields);
|
|
||||||
void imageTypesChanged(QList<QString> newImageTypes);
|
|
||||||
void includeItemTypesChanged(const QList<QString> &newIncludeItemTypes);
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
/**
|
|
||||||
* @brief (Re)loads the data into this model. This might make a network request.
|
|
||||||
*/
|
|
||||||
void reload();
|
|
||||||
protected:
|
protected:
|
||||||
|
// AbstractItemModel bookkeeping
|
||||||
enum LoadType {
|
|
||||||
RELOAD,
|
|
||||||
LOAD_MORE
|
|
||||||
};
|
|
||||||
|
|
||||||
void load(LoadType loadType);
|
|
||||||
/**
|
|
||||||
* @brief Adds parameters to the query
|
|
||||||
* @param query The query to add parameters to
|
|
||||||
*
|
|
||||||
* This method is intended to be overrided by subclasses. It gets called
|
|
||||||
* before a request is made to the server and can be used to enable
|
|
||||||
* query types specific for a certain model to be available.
|
|
||||||
*/
|
|
||||||
virtual void addQueryParameters(QUrlQuery &query);
|
|
||||||
ApiClient *m_apiClient = nullptr;
|
|
||||||
ModelStatus m_status = Uninitialised;
|
|
||||||
|
|
||||||
QString m_path;
|
|
||||||
QJsonArray m_array;
|
|
||||||
bool m_hasRecordResponse;
|
|
||||||
|
|
||||||
// Path properties
|
|
||||||
QString m_show;
|
|
||||||
|
|
||||||
// Query/record controlling properties
|
|
||||||
int m_limit = -1;
|
|
||||||
int m_startIndex = 0;
|
|
||||||
int m_totalRecordCount = 0;
|
|
||||||
const int DEFAULT_LIMIT = 100;
|
|
||||||
|
|
||||||
// Query properties
|
|
||||||
bool m_addUserId = false;
|
|
||||||
QString m_parentId;
|
|
||||||
QString m_seasonId;
|
|
||||||
QList<QString> m_fields = {};
|
|
||||||
QList<QString> m_imageTypes = {};
|
|
||||||
QList<QString> m_sortBy = {};
|
|
||||||
QList<QString> m_includeItemTypes = {};
|
|
||||||
SortOrder m_sortOrder = Unspecified;
|
|
||||||
bool m_recursive = false;
|
|
||||||
|
|
||||||
QHash<int, QByteArray> m_roles;
|
QHash<int, QByteArray> m_roles;
|
||||||
|
|
||||||
void setStatus(ModelStatus newStatus) {
|
// Helper methods.
|
||||||
this->m_status = newStatus;
|
T *deserializeResult(QJsonValueRef source);
|
||||||
emit this->statusChanged(newStatus);
|
virtual void addQueryParameters(QUrlQuery &query) override;
|
||||||
}
|
virtual void replacePathPlaceholders(QString &path) override;
|
||||||
|
|
||||||
|
virtual void setModelData(QJsonArray &data) override;
|
||||||
|
virtual void appendModelData(QJsonArray &data) override;
|
||||||
|
|
||||||
|
// Model-specific properties.
|
||||||
|
QList<T*> m_array;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/**
|
/**
|
||||||
|
@ -256,7 +307,7 @@ private:
|
||||||
/**
|
/**
|
||||||
* @brief List of the public users on the server.
|
* @brief List of the public users on the server.
|
||||||
*/
|
*/
|
||||||
class PublicUserModel : public ApiModel {
|
class PublicUserModel : public ApiModel<User> {
|
||||||
public:
|
public:
|
||||||
explicit PublicUserModel (QObject *parent = nullptr);
|
explicit PublicUserModel (QObject *parent = nullptr);
|
||||||
};
|
};
|
||||||
|
@ -266,15 +317,54 @@ public:
|
||||||
*
|
*
|
||||||
* Listens for updates in the library and updates the model accordingly.
|
* Listens for updates in the library and updates the model accordingly.
|
||||||
*/
|
*/
|
||||||
class ItemModel : public ApiModel {
|
class ItemModel : public ApiModel<Item> {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit ItemModel (QString path, bool responseHasRecords, bool replaceUser, QObject *parent = nullptr);
|
explicit ItemModel (QString path, bool responseHasRecords, bool replaceUser, QObject *parent = nullptr);
|
||||||
|
// Query parameters
|
||||||
|
Q_PROPERTY(QString parentId MEMBER m_parentId WRITE setParentId NOTIFY parentIdChanged)
|
||||||
|
Q_PROPERTY(QString seasonId MEMBER m_seasonId NOTIFY seasonIdChanged)
|
||||||
|
Q_PROPERTY(QList<QString> imageTypes MEMBER m_imageTypes NOTIFY imageTypesChanged)
|
||||||
|
Q_PROPERTY(QList<QString> includeItemTypes MEMBER m_includeItemTypes NOTIFY includeItemTypesChanged)
|
||||||
|
Q_PROPERTY(bool recursive MEMBER m_recursive)
|
||||||
|
QList<QString> m_includeItemTypes = {};
|
||||||
|
|
||||||
|
// Path properties
|
||||||
|
Q_PROPERTY(QString show MEMBER m_show NOTIFY showChanged)
|
||||||
|
|
||||||
|
void setParentId(const QString &parentId) {
|
||||||
|
m_parentId = parentId;
|
||||||
|
emit parentIdChanged(m_parentId);
|
||||||
|
}
|
||||||
|
signals:
|
||||||
|
// Query property signals
|
||||||
|
void parentIdChanged(QString newParentId);
|
||||||
|
void seasonIdChanged(QString newSeasonId);
|
||||||
|
void imageTypesChanged(QList<QString> newImageTypes);
|
||||||
|
void includeItemTypesChanged(const QList<QString> &newIncludeItemTypes);
|
||||||
|
|
||||||
|
// Path property signals
|
||||||
|
void showChanged(QString newShow);
|
||||||
public slots:
|
public slots:
|
||||||
void onUserDataChanged(const QString &itemId, QSharedPointer<DTO::UserData> userData);
|
void onUserDataChanged(const QString &itemId, DTO::UserData *userData);
|
||||||
|
protected:
|
||||||
|
virtual void addQueryParameters(QUrlQuery &query) override;
|
||||||
|
virtual void replacePathPlaceholders(QString &path) override;
|
||||||
|
private:
|
||||||
|
// Path properties
|
||||||
|
QString m_show;
|
||||||
|
|
||||||
|
// Query parameters
|
||||||
|
QString m_parentId;
|
||||||
|
QString m_seasonId;
|
||||||
|
QList<QString> m_imageTypes = {};
|
||||||
|
bool m_recursive = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
class UserViewModel : public ApiModel {
|
//template<>
|
||||||
|
//void ApiModel<Item>::apiClientChanged();
|
||||||
|
|
||||||
|
class UserViewModel : public ItemModel {
|
||||||
public:
|
public:
|
||||||
explicit UserViewModel (QObject *parent = nullptr);
|
explicit UserViewModel (QObject *parent = nullptr);
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,6 +31,7 @@ namespace Jellyfin {
|
||||||
|
|
||||||
namespace JsonHelper {
|
namespace JsonHelper {
|
||||||
void convertToCamelCase(QJsonValueRef val);
|
void convertToCamelCase(QJsonValueRef val);
|
||||||
|
void convertToCamelCase(QJsonValue &val);
|
||||||
QString convertToCamelCaseHelper(const QString &str);
|
QString convertToCamelCaseHelper(const QString &str);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,29 +20,38 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
#ifndef JELLYFIN_MEDIA_SOURCE_H
|
#ifndef JELLYFIN_MEDIA_SOURCE_H
|
||||||
#define JELLYFIN_MEDIA_SOURCE_H
|
#define JELLYFIN_MEDIA_SOURCE_H
|
||||||
|
|
||||||
|
#include <QAbstractItemModel>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QFuture>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
#include <QUrlQuery>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
#include <QUrlQuery>
|
|
||||||
|
|
||||||
#include <QtMultimedia/QMediaPlayer>
|
#include <QtMultimedia/QMediaPlayer>
|
||||||
|
#include <QtMultimedia/QMediaPlaylist>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
#include "JellyfinQt/DTO/item.h"
|
#include "JellyfinQt/DTO/item.h"
|
||||||
|
|
||||||
#include "apiclient.h"
|
#include "apiclient.h"
|
||||||
|
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
|
|
||||||
// Forward declaration of Jellyfin::ApiClient found in jellyfinapiclient.h
|
// Forward declaration of Jellyfin::ApiClient found in jellyfinapiclient.h
|
||||||
class ApiClient;
|
class ApiClient;
|
||||||
|
class ItemModel;
|
||||||
using namespace DTO;
|
using namespace DTO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The PlaybackManager class manages the playback of Jellyfin items. It fetches streams based on Jellyfin items, posts
|
* @brief The PlaybackManager class manages the playback of Jellyfin items. It fetches streams based on Jellyfin items, posts
|
||||||
* the current playback state to the Jellyfin Server and so on.
|
* the current playback state to the Jellyfin Server, contains the actual media player and so on.
|
||||||
|
*
|
||||||
|
* The PlaybackManager actually keeps two mediaPlayers, m_mediaPlayer1 and m_mediaPlayer2. When one is playing, the other is
|
||||||
|
* preloading the next item in the queue. The current media player is pointed to by m_mediaPlayer.
|
||||||
*/
|
*/
|
||||||
class PlaybackManager : public QObject, public QQmlParserStatus {
|
class PlaybackManager : public QObject, public QQmlParserStatus {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -54,28 +63,49 @@ public:
|
||||||
DirectPlay
|
DirectPlay
|
||||||
};
|
};
|
||||||
Q_ENUM(PlayMethod)
|
Q_ENUM(PlayMethod)
|
||||||
|
using FetchCallback = std::function<void(QUrl &&, PlayMethod)>;
|
||||||
|
|
||||||
explicit PlaybackManager(QObject *parent = nullptr);
|
explicit PlaybackManager(QObject *parent = nullptr);
|
||||||
|
|
||||||
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient)
|
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient)
|
||||||
Q_PROPERTY(Item *item READ item WRITE setItem NOTIFY itemChanged)
|
|
||||||
Q_PROPERTY(QString streamUrl READ streamUrl NOTIFY streamUrlChanged)
|
Q_PROPERTY(QString streamUrl READ streamUrl NOTIFY streamUrlChanged)
|
||||||
Q_PROPERTY(bool autoOpen MEMBER m_autoOpen NOTIFY autoOpenChanged)
|
Q_PROPERTY(bool autoOpen MEMBER m_autoOpen NOTIFY autoOpenChanged)
|
||||||
Q_PROPERTY(int audioIndex MEMBER m_audioIndex NOTIFY audioIndexChanged)
|
Q_PROPERTY(int audioIndex MEMBER m_audioIndex NOTIFY audioIndexChanged)
|
||||||
Q_PROPERTY(int subtitleIndex MEMBER m_subtitleIndex NOTIFY subtitleIndexChanged)
|
Q_PROPERTY(int subtitleIndex MEMBER m_subtitleIndex NOTIFY subtitleIndexChanged)
|
||||||
Q_PROPERTY(bool resumePlayback MEMBER m_resumePlayback NOTIFY resumePlaybackChanged)
|
Q_PROPERTY(bool resumePlayback MEMBER m_resumePlayback NOTIFY resumePlaybackChanged)
|
||||||
Q_PROPERTY(QObject* mediaPlayer READ mediaPlayer WRITE setMediaPlayer NOTIFY mediaPlayerChanged)
|
|
||||||
Q_PROPERTY(PlayMethod playMethod READ playMethod NOTIFY playMethodChanged)
|
Q_PROPERTY(PlayMethod playMethod READ playMethod NOTIFY playMethodChanged)
|
||||||
|
|
||||||
Item *item() const { return m_item; }
|
// Current Item and queue informatoion
|
||||||
void setItem(Item *newItem);
|
Q_PROPERTY(Item *item READ item NOTIFY itemChanged)
|
||||||
|
Q_PROPERTY(QAbstractItemModel *queue READ queue NOTIFY queueChanged)
|
||||||
|
Q_PROPERTY(int queueIndex READ queueIndex NOTIFY queueIndexChanged)
|
||||||
|
|
||||||
QObject *mediaPlayer() const {
|
// Current media player related property getters
|
||||||
return m_qmlMediaPlayer;
|
Q_PROPERTY(qint64 duration READ duration NOTIFY durationChanged)
|
||||||
}
|
Q_PROPERTY(QMediaPlayer::Error error READ error NOTIFY errorChanged)
|
||||||
void setMediaPlayer(QObject *qmlMediaPlayer);
|
Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged)
|
||||||
|
Q_PROPERTY(bool hasVideo READ hasVideo NOTIFY hasVideoChanged)
|
||||||
|
Q_PROPERTY(QObject* mediaObject READ mediaObject NOTIFY mediaObjectChanged)
|
||||||
|
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)
|
||||||
|
|
||||||
|
Item *item() const { return m_item; }
|
||||||
|
|
||||||
QString streamUrl() const { return m_streamUrl; }
|
QString streamUrl() const { return m_streamUrl; }
|
||||||
PlayMethod playMethod() const { return m_playMethod; }
|
PlayMethod playMethod() const { return m_playMethod; }
|
||||||
|
QObject *mediaObject() const { return m_mediaPlayer; }
|
||||||
|
qint64 position() const { return m_mediaPlayer->position(); }
|
||||||
|
qint64 duration() const { return m_mediaPlayer->duration(); }
|
||||||
|
ItemModel *queue() const { return m_queue; }
|
||||||
|
int queueIndex() const { return m_queueIndex; }
|
||||||
|
|
||||||
|
// Current media player related property getters
|
||||||
|
QMediaPlayer::State playbackState() const { return m_playbackState; }
|
||||||
|
QMediaPlayer::MediaStatus mediaStatus() const { return m_mediaPlayer->mediaStatus(); }
|
||||||
|
bool hasVideo() const { return m_mediaPlayer->isVideoAvailable(); }
|
||||||
|
QMediaPlayer::Error error () const { return m_mediaPlayer->error(); }
|
||||||
|
QString errorString() const { return m_mediaPlayer->errorString(); }
|
||||||
signals:
|
signals:
|
||||||
void itemChanged(Item *newItemId);
|
void itemChanged(Item *newItemId);
|
||||||
void streamUrlChanged(const QString &newStreamUrl);
|
void streamUrlChanged(const QString &newStreamUrl);
|
||||||
|
@ -86,14 +116,29 @@ signals:
|
||||||
void resumePlaybackChanged(bool newResumePlayback);
|
void resumePlaybackChanged(bool newResumePlayback);
|
||||||
void playMethodChanged(PlayMethod newPlayMethod);
|
void playMethodChanged(PlayMethod newPlayMethod);
|
||||||
|
|
||||||
|
// Current media player related property signals
|
||||||
|
void mediaObjectChanged(QObject *newMediaObject);
|
||||||
|
void positionChanged(qint64 newPosition);
|
||||||
|
void durationChanged(qint64 newDuration);
|
||||||
|
void queueChanged(ItemModel *newQue);
|
||||||
|
void queueIndexChanged(int newIndex);
|
||||||
|
void playbackStateChanged(QMediaPlayer::State newState);
|
||||||
|
void mediaStatusChanged(QMediaPlayer::MediaStatus newMediaStatus);
|
||||||
|
void hasVideoChanged(bool newHasVideo);
|
||||||
|
void errorChanged(QMediaPlayer::Error newError);
|
||||||
|
void errorStringChanged(const QString &newErrorString);
|
||||||
public slots:
|
public slots:
|
||||||
void updatePlaybackInfo();
|
|
||||||
/**
|
/**
|
||||||
* @brief playItem Plays the item with the given id. This will construct the Jellyfin::Item internally
|
* @brief playItem Plays the item with the given id. This will construct the Jellyfin::Item internally
|
||||||
* and delete it later.
|
* and delete it later.
|
||||||
* @param itemId The id of the item to play.
|
* @param itemId The id of the item to play.
|
||||||
*/
|
*/
|
||||||
void playItem(const QString &itemId);
|
void playItem(const QString &itemId);
|
||||||
|
void playItemInList(ItemModel *itemList, int index);
|
||||||
|
void play() { m_mediaPlayer->play(); }
|
||||||
|
void pause() { m_mediaPlayer->pause(); }
|
||||||
|
void seek(qint64 pos) { m_mediaPlayer->setPosition(pos); }
|
||||||
|
void stop() { m_mediaPlayer->stop(); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief previous Play the previous track in the current playlist.
|
* @brief previous Play the previous track in the current playlist.
|
||||||
|
@ -104,10 +149,16 @@ public slots:
|
||||||
* @brief next Play the next track in the current playlist.
|
* @brief next Play the next track in the current playlist.
|
||||||
*/
|
*/
|
||||||
void next();
|
void next();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void mediaPlayerStateChanged(QMediaPlayer::State newState);
|
void mediaPlayerStateChanged(QMediaPlayer::State newState);
|
||||||
void mediaPlayerPositionChanged(qint64 position);
|
void mediaPlayerPositionChanged(qint64 position);
|
||||||
void mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus);
|
void mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus);
|
||||||
|
void mediaPlayerError(QMediaPlayer::Error error);
|
||||||
|
/**
|
||||||
|
* @brief updatePlaybackInfo Updates the Jellyfin server with the current playback progress etc.
|
||||||
|
*/
|
||||||
|
void updatePlaybackInfo();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QTimer m_updateTimer;
|
QTimer m_updateTimer;
|
||||||
|
@ -122,10 +173,19 @@ private:
|
||||||
qint64 m_stopPosition = 0;
|
qint64 m_stopPosition = 0;
|
||||||
QMediaPlayer::State m_oldState = QMediaPlayer::StoppedState;
|
QMediaPlayer::State m_oldState = QMediaPlayer::StoppedState;
|
||||||
PlayMethod m_playMethod = Transcode;
|
PlayMethod m_playMethod = Transcode;
|
||||||
QObject *m_qmlMediaPlayer = nullptr;
|
QMediaPlayer::State m_playbackState = QMediaPlayer::StoppedState;
|
||||||
|
// Pointer to the current media player.
|
||||||
QMediaPlayer *m_mediaPlayer = nullptr;
|
QMediaPlayer *m_mediaPlayer = nullptr;
|
||||||
|
|
||||||
|
QMediaPlayer *m_mediaPlayer1;
|
||||||
|
QMediaPlayer *m_mediaPlayer2;
|
||||||
|
ItemModel *m_queue = nullptr;
|
||||||
|
int m_queueIndex = 0;
|
||||||
bool m_resumePlayback = true;
|
bool m_resumePlayback = true;
|
||||||
|
|
||||||
|
void setItem(Item *newItem);
|
||||||
|
void swapMediaPlayer();
|
||||||
|
|
||||||
bool m_qmlIsParsingComponent = false;
|
bool m_qmlIsParsingComponent = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -136,8 +196,13 @@ private:
|
||||||
/**
|
/**
|
||||||
* @brief Retrieves the URL of the stream to open.
|
* @brief Retrieves the URL of the stream to open.
|
||||||
*/
|
*/
|
||||||
void fetchStreamUrl();
|
void fetchStreamUrl(const Item *item, bool autoOpen, const FetchCallback &callback);
|
||||||
|
void fetchAndSetStreamUrl(const Item *item);
|
||||||
void setStreamUrl(const QString &streamUrl);
|
void setStreamUrl(const QString &streamUrl);
|
||||||
|
void setPlaybackState(QMediaPlayer::State newState);
|
||||||
|
|
||||||
|
Item *nextItem();
|
||||||
|
void setQueue(ItemModel *itemModel);
|
||||||
|
|
||||||
// Factor to multiply with when converting from milliseconds to ticks.
|
// Factor to multiply with when converting from milliseconds to ticks.
|
||||||
const static int MS_TICK_FACTOR = 10000;
|
const static int MS_TICK_FACTOR = 10000;
|
||||||
|
@ -149,6 +214,7 @@ private:
|
||||||
*/
|
*/
|
||||||
void postPlaybackInfo(PlaybackInfoType type);
|
void postPlaybackInfo(PlaybackInfoType type);
|
||||||
|
|
||||||
|
|
||||||
void classBegin() override {
|
void classBegin() override {
|
||||||
m_qmlIsParsingComponent = true;
|
m_qmlIsParsingComponent = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,16 @@ namespace Jellyfin {
|
||||||
namespace DTO {
|
namespace DTO {
|
||||||
|
|
||||||
const QRegularExpression JsonSerializable::m_listExpression = QRegularExpression("^QList<\\s*([a-zA-Z0-9]*)\\s*\\*?\\s*>$");
|
const QRegularExpression JsonSerializable::m_listExpression = QRegularExpression("^QList<\\s*([a-zA-Z0-9]*)\\s*\\*?\\s*>$");
|
||||||
|
const QRegularExpression JsonSerializable::m_hashExpression = QRegularExpression("^QHash<\\s*([a-zA-Z0-9]*)\\s*\\*?\\s*,\\s*([a-zA-Z0-9]*)\\s*\\*?\\s*>$");
|
||||||
JsonSerializable::JsonSerializable(QObject *parent) : QObject(parent) { }
|
JsonSerializable::JsonSerializable(QObject *parent) : QObject(parent) { }
|
||||||
|
JsonSerializable::~JsonSerializable() {
|
||||||
|
if (parent() == nullptr) {
|
||||||
|
qDebug() << "Deleting" << metaObject()->className() << ", parent: nullptr, ownership: " << QQmlEngine::objectOwnership(this);
|
||||||
|
} else {
|
||||||
|
qDebug() << "Deleting" << metaObject()->className() << ", parent: " << parent()->metaObject()->className()
|
||||||
|
<< ", ownership: " << QQmlEngine::objectOwnership(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void JsonSerializable::deserialize(const QJsonObject &jObj) {
|
void JsonSerializable::deserialize(const QJsonObject &jObj) {
|
||||||
const QMetaObject *obj = this->metaObject();
|
const QMetaObject *obj = this->metaObject();
|
||||||
|
@ -95,28 +104,24 @@ QVariant JsonSerializable::jsonToVariant(QMetaProperty prop, const QJsonValue &v
|
||||||
JsonHelper::convertToCamelCase(QJsonValueRef(&tmp, 0));
|
JsonHelper::convertToCamelCase(QJsonValueRef(&tmp, 0));
|
||||||
return QVariant(innerObj);
|
return QVariant(innerObj);
|
||||||
} else {
|
} else {
|
||||||
return deserializeQobject(innerObj, prop);
|
return deserializeQObject(innerObj, prop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant JsonSerializable::deserializeQobject(const QJsonObject &innerObj, const QMetaProperty &prop) {
|
QVariant JsonSerializable::deserializeQObject(const QJsonObject &innerObj, const QMetaProperty &prop) {
|
||||||
int typeNo = prop.userType();
|
int typeNo = prop.userType();
|
||||||
const QMetaObject *metaType = QMetaType::metaObjectForType(prop.userType());
|
const QMetaObject *metaType = QMetaType::metaObjectForType(prop.userType());
|
||||||
if (metaType == nullptr) {
|
if (metaType == nullptr) {
|
||||||
// Try to determine if the type is a qlist
|
// Try to determine if the type is a qlist
|
||||||
QRegularExpressionMatch match = m_listExpression.match(prop.typeName());
|
QRegularExpressionMatch listMatch = m_listExpression.match(prop.typeName());
|
||||||
if (match.hasMatch()) {
|
QRegularExpressionMatch hashMatch = m_hashExpression.match(prop.typeName());
|
||||||
|
if (listMatch.hasMatch()) {
|
||||||
// It is a qList! Now extract the inner type
|
// It is a qList! Now extract the inner type
|
||||||
// There should be an easier way, shouldn't there?
|
// There should be an easier way, shouldn't there?
|
||||||
QString listType = match.captured(1).prepend("Jellyfin::DTO::").append("*");
|
QString listType = listMatch.captured(1);
|
||||||
// UGLY CODE HERE WE COME
|
typeNo = findTypeIdForProperty(listType);
|
||||||
typeNo = QMetaType::type(listType.toUtf8());
|
|
||||||
if (typeNo == QMetaType::UnknownType) {
|
|
||||||
qDebug() << "Unknown type: " << listType;
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
metaType = QMetaType::metaObjectForType(typeNo);
|
metaType = QMetaType::metaObjectForType(typeNo);
|
||||||
} else {
|
} else {
|
||||||
qDebug() << "No metaObject for " << prop.typeName() << ", " << prop.type() << ", " << prop.userType();
|
qDebug() << "No metaObject for " << prop.typeName() << ", " << prop.type() << ", " << prop.userType();
|
||||||
|
@ -126,7 +131,7 @@ QVariant JsonSerializable::deserializeQobject(const QJsonObject &innerObj, const
|
||||||
QObject *deserializedInnerObj = metaType->newInstance();
|
QObject *deserializedInnerObj = metaType->newInstance();
|
||||||
deserializedInnerObj->setParent(this);
|
deserializedInnerObj->setParent(this);
|
||||||
if (JsonSerializable *ser = dynamic_cast<JsonSerializable *>(deserializedInnerObj)) {
|
if (JsonSerializable *ser = dynamic_cast<JsonSerializable *>(deserializedInnerObj)) {
|
||||||
qDebug() << "Deserializing user type " << deserializedInnerObj->metaObject()->className();
|
// qDebug() << "Deserializing user type " << deserializedInnerObj->metaObject()->className();
|
||||||
ser->deserialize(innerObj);
|
ser->deserialize(innerObj);
|
||||||
return QVariant(typeNo, &ser);
|
return QVariant(typeNo, &ser);
|
||||||
} else {
|
} else {
|
||||||
|
@ -136,6 +141,20 @@ QVariant JsonSerializable::deserializeQobject(const QJsonObject &innerObj, const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int JsonSerializable::findTypeIdForProperty(QString type) {
|
||||||
|
// UGLY CODE HERE WE COME
|
||||||
|
// We assume the type is either in no namespace (Qt Types) or in the Jellyfin::DTO namespace.
|
||||||
|
int typeNo = QMetaType::type(type.toUtf8());
|
||||||
|
if (typeNo == QMetaType::UnknownType) {
|
||||||
|
typeNo = QMetaType::type(type.prepend("Jellyfin::DTO::").append("*").toUtf8());
|
||||||
|
if (typeNo == QMetaType::UnknownType) {
|
||||||
|
qDebug() << "Unknown type: " << type;
|
||||||
|
return typeNo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return typeNo;
|
||||||
|
}
|
||||||
|
|
||||||
QJsonObject JsonSerializable::serialize(bool capitalize) const {
|
QJsonObject JsonSerializable::serialize(bool capitalize) const {
|
||||||
QJsonObject result;
|
QJsonObject result;
|
||||||
const QMetaObject *obj = this->metaObject();
|
const QMetaObject *obj = this->metaObject();
|
||||||
|
@ -220,6 +239,13 @@ void RemoteData::setApiClient(ApiClient *newApiClient) {
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RemoteData::setExtraFields(const QStringList &extraFields) {
|
||||||
|
if (extraFields != m_extraFields) {
|
||||||
|
emit extraFieldsChanged(extraFields);
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void RemoteData::reload() {
|
void RemoteData::reload() {
|
||||||
if (!canReload() || m_apiClient == nullptr) {
|
if (!canReload() || m_apiClient == nullptr) {
|
||||||
setStatus(Uninitialised);
|
setStatus(Uninitialised);
|
||||||
|
@ -227,7 +253,11 @@ void RemoteData::reload() {
|
||||||
} else {
|
} else {
|
||||||
setStatus(Loading);
|
setStatus(Loading);
|
||||||
}
|
}
|
||||||
QNetworkReply *rep = m_apiClient->get(getDataUrl());
|
QUrlQuery params;
|
||||||
|
if (m_extraFields.length() > 0) {
|
||||||
|
params.addQueryItem("fields", m_extraFields.join(","));
|
||||||
|
}
|
||||||
|
QNetworkReply *rep = m_apiClient->get(getDataUrl() + QStringLiteral("?") + params.toString(QUrl::EncodeReserved));
|
||||||
connect(rep, &QNetworkReply::finished, this, [this, rep]() {
|
connect(rep, &QNetworkReply::finished, this, [this, rep]() {
|
||||||
rep->deleteLater();
|
rep->deleteLater();
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ QString Item::getDataUrl() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Item::canReload() const {
|
bool Item::canReload() const {
|
||||||
return !m_id.isNull();
|
return !m_id.isNull() && m_apiClient != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Item::setJellyfinId(QString newId) {
|
void Item::setJellyfinId(QString newId) {
|
||||||
|
@ -54,7 +54,7 @@ void Item::setJellyfinId(QString newId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Item::onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData) {
|
void Item::onUserDataChanged(const QString &itemId, UserData *userData) {
|
||||||
if (itemId != m_id || m_userData == nullptr) return;
|
if (itemId != m_id || m_userData == nullptr) return;
|
||||||
m_userData->onUpdated(userData);
|
m_userData->onUpdated(userData);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ void UserData::updateOnServer() {
|
||||||
//TODO: implement
|
//TODO: implement
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserData::onUpdated(QSharedPointer<UserData> other) {
|
void UserData::onUpdated(UserData *other) {
|
||||||
// The reason I'm not using setLikes and similar is that they don't work with std::nullopt,
|
// The reason I'm not using setLikes and similar is that they don't work with std::nullopt,
|
||||||
// since QML does not like it.
|
// since QML does not like it.
|
||||||
// THe other reason is that the setLikes method will send a post request to the server, to update the contents
|
// THe other reason is that the setLikes method will send a post request to the server, to update the contents
|
||||||
|
|
|
@ -45,7 +45,7 @@ void ApiClient::addBaseRequestHeaders(QNetworkRequest &request, const QString &p
|
||||||
request.setRawHeader("Accept", "application/json;"); // profile=\"CamelCase\"");
|
request.setRawHeader("Accept", "application/json;"); // profile=\"CamelCase\"");
|
||||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Sailfin/%1").arg(version()));
|
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Sailfin/%1").arg(version()));
|
||||||
QString url = this->m_baseUrl + path;
|
QString url = this->m_baseUrl + path;
|
||||||
if (!params.isEmpty()) url += "?" + params.toString();
|
if (!params.isEmpty()) url += "?" + params.toString(QUrl::EncodeReserved);
|
||||||
request.setUrl(url);
|
request.setUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,7 +263,7 @@ void ApiClient::defaultNetworkErrorHandler(QNetworkReply::NetworkError error) {
|
||||||
rep->deleteLater();
|
rep->deleteLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApiClient::onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData) {
|
void ApiClient::onUserDataChanged(const QString &itemId, UserData *userData) {
|
||||||
emit userDataChanged(itemId, userData);
|
emit userDataChanged(itemId, userData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,81 +19,70 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
#include "JellyfinQt/apimodel.h"
|
#include "JellyfinQt/apimodel.h"
|
||||||
|
|
||||||
|
#include "JellyfinQt/DTO/item.h"
|
||||||
#include "JellyfinQt/DTO/userdata.h"
|
#include "JellyfinQt/DTO/userdata.h"
|
||||||
|
#include "JellyfinQt/DTO/user.h"
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
ApiModel::ApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent)
|
|
||||||
|
// BaseApiModel
|
||||||
|
|
||||||
|
BaseApiModel::BaseApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent)
|
||||||
: QAbstractListModel(parent),
|
: QAbstractListModel(parent),
|
||||||
m_path(path),
|
m_path(path),
|
||||||
m_hasRecordResponse(hasRecordResponse),
|
m_hasRecordResponse(hasRecordResponse),
|
||||||
m_addUserId(addUserId) {
|
m_addUserId(addUserId) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApiModel::reload() {
|
void BaseApiModel::setApiClient(ApiClient *apiClient) {
|
||||||
|
m_apiClient = apiClient;
|
||||||
|
emit apiClientChanged(m_apiClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BaseApiModel::setLimit(int newLimit) {
|
||||||
|
m_limit = newLimit;
|
||||||
|
emit limitChanged(newLimit);
|
||||||
|
if (m_apiClient != nullptr && !m_isBeingParsed) {
|
||||||
|
load(LOAD_MORE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BaseApiModel::reload() {
|
||||||
this->setStatus(Loading);
|
this->setStatus(Loading);
|
||||||
m_startIndex = 0;
|
m_startIndex = 0;
|
||||||
load(RELOAD);
|
load(RELOAD);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApiModel::load(LoadType type) {
|
|
||||||
|
void BaseApiModel::load(LoadType type) {
|
||||||
qDebug() << (type == RELOAD ? "RELOAD" : "LOAD_MORE");
|
qDebug() << (type == RELOAD ? "RELOAD" : "LOAD_MORE");
|
||||||
if (m_apiClient == nullptr) {
|
if (m_apiClient == nullptr) {
|
||||||
qWarning() << "Please set the apiClient property before (re)loading";
|
qWarning() << "Please set the apiClient property before (re)loading";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (m_path.contains("{{user}}")) {
|
|
||||||
m_path = m_path.replace("{{user}}", m_apiClient->userId());
|
QString path(m_path);
|
||||||
}
|
replacePathPlaceholders(path);
|
||||||
if (m_path.contains("{{show}}") && !m_show.isEmpty()) {
|
|
||||||
m_path = m_path.replace("{{show}}", m_show);
|
|
||||||
}
|
|
||||||
QUrlQuery query;
|
QUrlQuery query;
|
||||||
if (m_limit >= 0) {
|
|
||||||
query.addQueryItem("Limit", QString::number(m_limit));
|
|
||||||
} else {
|
|
||||||
query.addQueryItem("Limit", QString::number(DEFAULT_LIMIT));
|
|
||||||
}
|
|
||||||
if (m_startIndex > 0) {
|
|
||||||
query.addQueryItem("StartIndex", QString::number(m_startIndex));
|
|
||||||
}
|
|
||||||
if (!m_parentId.isEmpty()) {
|
|
||||||
query.addQueryItem("ParentId", m_parentId);
|
|
||||||
}
|
|
||||||
if (!m_sortBy.empty()) {
|
|
||||||
query.addQueryItem("SortBy", m_sortBy.join(","));
|
|
||||||
}
|
|
||||||
if (m_sortOrder != Unspecified) {
|
|
||||||
query.addQueryItem("SortOrder", m_sortOrder == Ascending ? "Ascending" : "Descending");
|
|
||||||
}
|
|
||||||
if (!m_imageTypes.empty()) {
|
|
||||||
query.addQueryItem("ImageTypes", m_imageTypes.join(","));
|
|
||||||
}
|
|
||||||
if (!m_includeItemTypes.empty()) {
|
|
||||||
query.addQueryItem("IncludeItemTypes", m_includeItemTypes.join(","));
|
|
||||||
}
|
|
||||||
if (!m_fields.empty()) {
|
|
||||||
query.addQueryItem("Fields", m_fields.join(","));
|
|
||||||
}
|
|
||||||
if (!m_seasonId.isEmpty()) {
|
|
||||||
query.addQueryItem("seasonId", m_seasonId);
|
|
||||||
}
|
|
||||||
if (m_addUserId) {
|
|
||||||
query.addQueryItem("userId", m_apiClient->userId());
|
|
||||||
}
|
|
||||||
if (m_recursive) {
|
|
||||||
query.addQueryItem("Recursive", "true");
|
|
||||||
}
|
|
||||||
addQueryParameters(query);
|
addQueryParameters(query);
|
||||||
QNetworkReply *rep = m_apiClient->get(m_path, query);
|
|
||||||
|
QNetworkReply *rep = m_apiClient->get(path, query);
|
||||||
connect(rep, &QNetworkReply::finished, this, [this, type, rep]() {
|
connect(rep, &QNetworkReply::finished, this, [this, type, rep]() {
|
||||||
|
qDebug() << rep->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << ": " << rep->request().url();
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(rep->readAll());
|
QJsonDocument doc = QJsonDocument::fromJson(rep->readAll());
|
||||||
|
if (doc.isNull()) {
|
||||||
|
qWarning() << "JSON parse error";
|
||||||
|
this->setStatus(Error);
|
||||||
|
}
|
||||||
if (!m_hasRecordResponse) {
|
if (!m_hasRecordResponse) {
|
||||||
if (!doc.isArray()) {
|
if (!doc.isArray()) {
|
||||||
qWarning() << "Object is not an array!";
|
qWarning() << "Object is not an array!";
|
||||||
this->setStatus(Error);
|
this->setStatus(Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this->m_array = doc.array();
|
QJsonArray items = doc.array();
|
||||||
|
setModelData(items);
|
||||||
} else {
|
} else {
|
||||||
if (!doc.isObject()) {
|
if (!doc.isObject()) {
|
||||||
qWarning() << "Object is not an object!";
|
qWarning() << "Object is not an object!";
|
||||||
|
@ -125,42 +114,109 @@ void ApiModel::load(LoadType type) {
|
||||||
QJsonArray items = obj["Items"].toArray();
|
QJsonArray items = obj["Items"].toArray();
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case RELOAD:
|
case RELOAD:
|
||||||
this->m_array = items;
|
setModelData(items);
|
||||||
break;
|
break;
|
||||||
case LOAD_MORE:
|
case LOAD_MORE:
|
||||||
this->beginInsertRows(QModelIndex(), m_array.size(), m_array.size() + items.size() - 1);
|
appendModelData(items);
|
||||||
// QJsonArray apparently doesn't allow concatenating lists like QList or std::vector
|
|
||||||
for (auto it = items.begin(); it != items.end(); it++) {
|
|
||||||
JsonHelper::convertToCamelCase(*it);
|
|
||||||
}
|
|
||||||
foreach (const QJsonValue &val, items) {
|
|
||||||
m_array.append(val);
|
|
||||||
}
|
|
||||||
this->endInsertRows();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (type == RELOAD) {
|
|
||||||
generateFields();
|
|
||||||
}
|
|
||||||
this->setStatus(Ready);
|
this->setStatus(Ready);
|
||||||
rep->deleteLater();
|
rep->deleteLater();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApiModel::generateFields() {
|
void BaseApiModel::addQueryParameters(QUrlQuery &query) {
|
||||||
if (m_array.size() == 0) return;
|
if (m_limit >= 0) {
|
||||||
this->beginResetModel();
|
query.addQueryItem("Limit", QString::number(m_limit));
|
||||||
|
} else {
|
||||||
|
query.addQueryItem("Limit", QString::number(DEFAULT_LIMIT));
|
||||||
|
}
|
||||||
|
if (m_startIndex > 0) {
|
||||||
|
query.addQueryItem("StartIndex", QString::number(m_startIndex));
|
||||||
|
}
|
||||||
|
if (!m_sortBy.empty()) {
|
||||||
|
query.addQueryItem("SortBy", m_sortBy.join(","));
|
||||||
|
}
|
||||||
|
if (m_sortOrder != Unspecified) {
|
||||||
|
query.addQueryItem("SortOrder", m_sortOrder == Ascending ? "Ascending" : "Descending");
|
||||||
|
}
|
||||||
|
if (!m_fields.empty()) {
|
||||||
|
query.addQueryItem("Fields", m_fields.join(","));
|
||||||
|
}
|
||||||
|
if (m_addUserId) {
|
||||||
|
query.addQueryItem("userId", m_apiClient->userId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BaseApiModel::replacePathPlaceholders(QString &path) {
|
||||||
|
if (path.contains("{{user}}")) {
|
||||||
|
path = path.replace("{{user}}", m_apiClient->userId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BaseApiModel::classBegin() {
|
||||||
|
m_isBeingParsed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BaseApiModel::componentComplete() {
|
||||||
|
m_isBeingParsed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApiModel
|
||||||
|
template <class T>
|
||||||
|
ApiModel<T>::ApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent)
|
||||||
|
: BaseApiModel(path, hasRecordResponse, addUserId, parent) {
|
||||||
|
// If based on QObject, we know our role names before the first request
|
||||||
|
generateFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
ApiModel<QJsonValue>::ApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent)
|
||||||
|
: BaseApiModel(path, hasRecordResponse, addUserId, parent) {
|
||||||
|
// But we only know our role names after our first request.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
T *ApiModel<T>::deserializeResult(QJsonValueRef source) {
|
||||||
|
T *result = new T(static_cast<BaseApiModel *>(this));
|
||||||
|
result->deserialize(source.toObject());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
QJsonValue *ApiModel<QJsonValue>::deserializeResult(QJsonValueRef source) {
|
||||||
|
QJsonValue *result = new QJsonValue(source);
|
||||||
|
JsonHelper::convertToCamelCase(*result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void ApiModel<T>::generateFields() {
|
||||||
|
const QMetaObject *obj = &T::staticMetaObject;
|
||||||
|
m_roles[Qt::UserRole + 1] = "qtObject";
|
||||||
|
for (int i = 0; i < obj->propertyCount(); i++) {
|
||||||
|
QMetaProperty property = obj->property(i);
|
||||||
|
m_roles.insert(Qt::UserRole + 2 + i, property.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
void ApiModel<QJsonValue>::generateFields() {
|
||||||
|
// We can only generate field names if there is a first item. Redefining role names later leads to
|
||||||
|
// unexpected results, so prevent it as well.
|
||||||
|
if (m_array.size() == 0 || m_roles.size() > 0) return;
|
||||||
int i = Qt::UserRole + 1;
|
int i = Qt::UserRole + 1;
|
||||||
if (!m_array[0].isObject()) {
|
if (!m_array[0]->isObject()) {
|
||||||
qWarning() << "Iterator is not an object?";
|
qWarning() << "Iterator is not an object?";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Walks over the keys in the first record and adds them to the rolenames.
|
// Walks over the keys in the first record and adds them to the rolenames.
|
||||||
// This assumes the back-end has the same keys for every record. I could technically
|
// This assumes the back-end has the same keys for every record. I could technically
|
||||||
// go over all records to be really sure, but no-one got time for a O(n²) algorithm, so
|
// go over all records to be really sure, but no-one got time for a O(n) algorithm, so
|
||||||
// this heuristic hopefully suffices.
|
// this heuristic hopefully suffices.
|
||||||
QJsonObject ob = m_array[0].toObject();
|
QJsonObject ob = m_array[0]->toObject();
|
||||||
for (auto jt = ob.begin(); jt != ob.end(); jt++) {
|
for (auto jt = ob.begin(); jt != ob.end(); jt++) {
|
||||||
QString keyName = jt.key();
|
QString keyName = jt.key();
|
||||||
keyName[0] = keyName[0].toLower();
|
keyName[0] = keyName[0].toLower();
|
||||||
|
@ -169,20 +225,78 @@ void ApiModel::generateFields() {
|
||||||
m_roles.insert(i++, keyArr);
|
m_roles.insert(i++, keyArr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (auto it = m_array.begin(); it != m_array.end(); it++){
|
}
|
||||||
JsonHelper::convertToCamelCase(*it);
|
|
||||||
|
template <class T>
|
||||||
|
void ApiModel<T>::setModelData(QJsonArray &data) {
|
||||||
|
this->beginResetModel();
|
||||||
|
for (T* value : m_array) {
|
||||||
|
value->deleteLater();
|
||||||
|
}
|
||||||
|
m_array.clear();
|
||||||
|
for(QJsonValueRef value : data) {
|
||||||
|
m_array.append(deserializeResult(value));
|
||||||
}
|
}
|
||||||
this->endResetModel();
|
this->endResetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant ApiModel::data(const QModelIndex &index, int role) const {
|
template <>
|
||||||
|
void ApiModel<QJsonValue>::setModelData(QJsonArray &data) {
|
||||||
|
generateFields();
|
||||||
|
this->beginResetModel();
|
||||||
|
for (QJsonValue* value : m_array) {
|
||||||
|
delete value;
|
||||||
|
}
|
||||||
|
m_array.clear();
|
||||||
|
for(QJsonValueRef value : data) {
|
||||||
|
m_array.append(deserializeResult(value));
|
||||||
|
}
|
||||||
|
this->endResetModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void ApiModel<T>::appendModelData(QJsonArray &data) {
|
||||||
|
this->beginInsertRows(QModelIndex(), m_array.size(), m_array.size() + data.size() - 1);
|
||||||
|
// QJsonArray apparently doesn't allow concatenating lists like QList or std::vector
|
||||||
|
for (auto it = data.begin(); it != data.end(); it++) {
|
||||||
|
JsonHelper::convertToCamelCase(*it);
|
||||||
|
}
|
||||||
|
for(QJsonValueRef val : data) {
|
||||||
|
m_array.append(deserializeResult(val));
|
||||||
|
}
|
||||||
|
this->endInsertRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
QVariant ApiModel<T>::data(const QModelIndex &index, int role) const {
|
||||||
// Ignore roles we don't know
|
// Ignore roles we don't know
|
||||||
if (role <= Qt::UserRole || role >= Qt::UserRole + m_roles.size()) return QVariant();
|
if (role <= Qt::UserRole || role >= Qt::UserRole + m_roles.size()) return QVariant();
|
||||||
// Ignore invalid indices.
|
// Ignore invalid indices.
|
||||||
if (!index.isValid()) return QVariant();
|
if (!index.isValid()) return QVariant();
|
||||||
|
|
||||||
|
|
||||||
QJsonObject obj = m_array.at(index.row()).toObject();
|
T* obj = m_array.at(index.row());
|
||||||
|
// m_roleNames[role] == "qtObject"
|
||||||
|
if (role == Qt::UserRole + 1) {
|
||||||
|
return QVariant::fromValue(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString &key = m_roles[role];
|
||||||
|
|
||||||
|
if (role - Qt::UserRole - 2 < obj->metaObject()->propertyCount() ) {
|
||||||
|
return obj->property(key.toLocal8Bit());
|
||||||
|
}
|
||||||
|
return QVariant();
|
||||||
|
}
|
||||||
|
template <>
|
||||||
|
QVariant ApiModel<QJsonValue>::data(const QModelIndex &index, int role) const {
|
||||||
|
// Ignore roles we don't know
|
||||||
|
if (role <= Qt::UserRole || role >= Qt::UserRole + m_roles.size()) return QVariant();
|
||||||
|
// Ignore invalid indices.
|
||||||
|
if (!index.isValid()) return QVariant();
|
||||||
|
|
||||||
|
|
||||||
|
QJsonObject obj = m_array.at(index.row())->toObject();
|
||||||
|
|
||||||
const QString &key = m_roles[role];
|
const QString &key = m_roles[role];
|
||||||
|
|
||||||
|
@ -192,7 +306,9 @@ QVariant ApiModel::data(const QModelIndex &index, int role) const {
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ApiModel::canFetchMore(const QModelIndex &parent) const {
|
|
||||||
|
template <class T>
|
||||||
|
bool ApiModel<T>::canFetchMore(const QModelIndex &parent) const {
|
||||||
if (parent.isValid()) return false;
|
if (parent.isValid()) return false;
|
||||||
switch(m_status) {
|
switch(m_status) {
|
||||||
case Uninitialised:
|
case Uninitialised:
|
||||||
|
@ -208,50 +324,99 @@ bool ApiModel::canFetchMore(const QModelIndex &parent) const {
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApiModel::fetchMore(const QModelIndex &parent) {
|
template <class T>
|
||||||
|
void ApiModel<T>::fetchMore(const QModelIndex &parent) {
|
||||||
if (parent.isValid()) return;
|
if (parent.isValid()) return;
|
||||||
this->setStatus(LoadingMore);
|
this->setStatus(LoadingMore);
|
||||||
load(LOAD_MORE);
|
load(LOAD_MORE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApiModel::addQueryParameters(QUrlQuery &query) { Q_UNUSED(query)}
|
template <class T>
|
||||||
|
void ApiModel<T>::addQueryParameters(QUrlQuery &query) {
|
||||||
|
BaseApiModel::addQueryParameters(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void ApiModel<T>::replacePathPlaceholders(QString &path) {
|
||||||
|
BaseApiModel::replacePathPlaceholders(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void ApiModel<T>::insert(int index, T* object) {
|
||||||
|
Q_ASSERT(index >=0 && index <= size());
|
||||||
|
this->beginInsertRows(QModelIndex(), index, index);
|
||||||
|
m_array.insert(index, object);
|
||||||
|
this->endInsertRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void ApiModel<T>::removeAt(int index) {
|
||||||
|
this->beginRemoveRows(QModelIndex(), index, index);
|
||||||
|
m_array.removeAt(index);
|
||||||
|
this->endRemoveRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void ApiModel<T>::removeOne(T* object) {
|
||||||
|
int idx = m_array.indexOf(object);
|
||||||
|
if (idx >= 0) {
|
||||||
|
removeAt(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Itemmodel
|
// Itemmodel
|
||||||
|
|
||||||
ItemModel::ItemModel(QString path, bool hasRecordFields, bool replaceUser, QObject *parent)
|
ItemModel::ItemModel(QString path, bool hasRecordFields, bool replaceUser, QObject *parent)
|
||||||
: ApiModel (path, hasRecordFields, replaceUser, parent){
|
: ApiModel (path, hasRecordFields, replaceUser, parent){
|
||||||
connect(this, &ApiModel::apiClientChanged, this, [this](ApiClient *newApiClient) {
|
QObject::connect(this, &BaseApiModel::apiClientChanged, static_cast<BaseApiModel *>(this), [this](ApiClient *newApiClient) {
|
||||||
connect(newApiClient, &ApiClient::userDataChanged, this, &ItemModel::onUserDataChanged);
|
QObject::connect(newApiClient, &ApiClient::userDataChanged, this, &ItemModel::onUserDataChanged);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ItemModel::onUserDataChanged(const QString &itemId, QSharedPointer<DTO::UserData> userData) {
|
void ItemModel::onUserDataChanged(const QString &itemId, DTO::UserData *userData) {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (QJsonValueRef val: m_array) {
|
for (Item *val: m_array) {
|
||||||
QJsonObject item = val.toObject();
|
if (val->userData() != nullptr && val->jellyfinId() == itemId) {
|
||||||
if (item.contains("id") && item["id"].toString() == itemId) {
|
|
||||||
if (item.contains("userData")) {
|
|
||||||
QModelIndex cell = this->index(i);
|
QModelIndex cell = this->index(i);
|
||||||
item["userData"] = userData->serialize(false);
|
val->userData()->onUpdated(userData);
|
||||||
val = item;
|
|
||||||
this->dataChanged(cell, cell);
|
this->dataChanged(cell, cell);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ItemModel::addQueryParameters(QUrlQuery &query) {
|
||||||
|
ApiModel<Item>::addQueryParameters(query);
|
||||||
|
if (!m_parentId.isEmpty()) {
|
||||||
|
query.addQueryItem("ParentId", m_parentId);
|
||||||
|
}
|
||||||
|
if (!m_imageTypes.empty()) {
|
||||||
|
query.addQueryItem("ImageTypes", m_imageTypes.join(","));
|
||||||
|
}
|
||||||
|
if (!m_includeItemTypes.empty()) {
|
||||||
|
query.addQueryItem("IncludeItemTypes", m_includeItemTypes.join(","));
|
||||||
|
}
|
||||||
|
if (!m_seasonId.isEmpty()) {
|
||||||
|
query.addQueryItem("seasonId", m_seasonId);
|
||||||
|
}
|
||||||
|
if (m_recursive) {
|
||||||
|
query.addQueryItem("Recursive", "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ItemModel::replacePathPlaceholders(QString &path) {
|
||||||
|
ApiModel::replacePathPlaceholders(path);
|
||||||
|
if (path.contains("{{show}}") && !m_show.isEmpty()) {
|
||||||
|
path = m_path.replace("{{show}}", m_show);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PublicUserModel::PublicUserModel(QObject *parent)
|
PublicUserModel::PublicUserModel(QObject *parent)
|
||||||
: ApiModel ("/users/public", false, false, parent) { }
|
: ApiModel ("/users/public", false, false, parent) { }
|
||||||
|
|
||||||
UserViewModel::UserViewModel(QObject *parent)
|
UserViewModel::UserViewModel(QObject *parent)
|
||||||
: ApiModel ("/Users/{{user}}/Views", true, false, parent) {}
|
: ItemModel ("/Users/{{user}}/Views", true, false, parent) {}
|
||||||
|
|
||||||
UserItemModel::UserItemModel(QObject *parent)
|
UserItemModel::UserItemModel(QObject *parent)
|
||||||
: ItemModel ("/Users/{{user}}/Items", true, false, parent) {}
|
: ItemModel ("/Users/{{user}}/Items", true, false, parent) {}
|
||||||
|
@ -272,7 +437,7 @@ ShowEpisodesModel::ShowEpisodesModel(QObject *parent)
|
||||||
: ItemModel ("/Shows/{{show}}/Episodes", true, true, parent) {}
|
: ItemModel ("/Shows/{{show}}/Episodes", true, true, parent) {}
|
||||||
|
|
||||||
void registerModels(const char *URI) {
|
void registerModels(const char *URI) {
|
||||||
qmlRegisterUncreatableType<ApiModel>(URI, 1, 0, "ApiModel", "Is enum and base class");
|
qmlRegisterUncreatableType<BaseApiModel>(URI, 1, 0, "ApiModel", "Is enum and base class");
|
||||||
qmlRegisterUncreatableType<SortOptions>(URI, 1, 0, "SortOptions", "Is enum");
|
qmlRegisterUncreatableType<SortOptions>(URI, 1, 0, "SortOptions", "Is enum");
|
||||||
qmlRegisterType<PublicUserModel>(URI, 1, 0, "PublicUserModel");
|
qmlRegisterType<PublicUserModel>(URI, 1, 0, "PublicUserModel");
|
||||||
qmlRegisterType<UserViewModel>(URI, 1, 0, "UserViewModel");
|
qmlRegisterType<UserViewModel>(URI, 1, 0, "UserViewModel");
|
||||||
|
|
|
@ -51,6 +51,37 @@ void convertToCamelCase(QJsonValueRef val) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
void convertToCamelCase(QJsonValue &val) {
|
||||||
|
switch(val.type()) {
|
||||||
|
case QJsonValue::Object: {
|
||||||
|
QJsonObject obj = val.toObject();
|
||||||
|
for(const QString &key: obj.keys()) {
|
||||||
|
QJsonValueRef ref = obj[key];
|
||||||
|
convertToCamelCase(ref);
|
||||||
|
obj[convertToCamelCaseHelper(key)] = ref;
|
||||||
|
if (key[0].isLower() || !key[0].isLetter()) {
|
||||||
|
obj[key] = ref;
|
||||||
|
} else {
|
||||||
|
obj[convertToCamelCaseHelper(key)] = ref;
|
||||||
|
obj.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val = obj;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case QJsonValue::Array: {
|
||||||
|
QJsonArray arr = val.toArray();
|
||||||
|
for (auto it = arr.begin(); it != arr.end(); it++) {
|
||||||
|
convertToCamelCase(*it);
|
||||||
|
}
|
||||||
|
val = arr;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
QString convertToCamelCaseHelper(const QString &str) {
|
QString convertToCamelCaseHelper(const QString &str) {
|
||||||
QString res(str);
|
QString res(str);
|
||||||
|
|
|
@ -19,25 +19,29 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
#include "JellyfinQt/playbackmanager.h"
|
#include "JellyfinQt/playbackmanager.h"
|
||||||
|
|
||||||
|
#include "JellyfinQt/apimodel.h"
|
||||||
|
|
||||||
#include "JellyfinQt/DTO/dto.h"
|
#include "JellyfinQt/DTO/dto.h"
|
||||||
#include "JellyfinQt/DTO/userdata.h"
|
#include "JellyfinQt/DTO/userdata.h"
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
|
class ItemModel;
|
||||||
|
|
||||||
PlaybackManager::PlaybackManager(QObject *parent)
|
PlaybackManager::PlaybackManager(QObject *parent)
|
||||||
: QObject(parent) {
|
: QObject(parent), m_mediaPlayer1(new QMediaPlayer(this)), m_mediaPlayer2(new QMediaPlayer(this)) {
|
||||||
|
// Set up connections.
|
||||||
|
swapMediaPlayer();
|
||||||
m_updateTimer.setInterval(10000); // 10 seconds
|
m_updateTimer.setInterval(10000); // 10 seconds
|
||||||
m_updateTimer.setSingleShot(false);
|
m_updateTimer.setSingleShot(false);
|
||||||
connect(&m_updateTimer, &QTimer::timeout, this, &PlaybackManager::updatePlaybackInfo);
|
connect(&m_updateTimer, &QTimer::timeout, this, &PlaybackManager::updatePlaybackInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::fetchStreamUrl() {
|
void PlaybackManager::fetchStreamUrl(const Item *item, bool autoOpen, const FetchCallback &callback) {
|
||||||
if (m_item == nullptr || m_apiClient == nullptr) {
|
if (item == nullptr || m_apiClient == nullptr) {
|
||||||
qDebug() << "Item or apiClient not set";
|
qDebug() << "Item or apiClient not set";
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
m_resumePosition = 0;
|
m_resumePosition = 0;
|
||||||
if (m_resumePlayback && !m_item->property("userData").isNull()) {
|
if (m_resumePlayback && !item->property("userData").isNull()) {
|
||||||
UserData* userData = qvariant_cast<UserData *>(m_item->property("userData"));
|
UserData* userData = qvariant_cast<UserData *>(m_item->property("userData"));
|
||||||
if (userData != nullptr) {
|
if (userData != nullptr) {
|
||||||
m_resumePosition = userData->playbackPositionTicks();
|
m_resumePosition = userData->playbackPositionTicks();
|
||||||
|
@ -47,16 +51,16 @@ void PlaybackManager::fetchStreamUrl() {
|
||||||
params.addQueryItem("UserId", m_apiClient->userId());
|
params.addQueryItem("UserId", m_apiClient->userId());
|
||||||
params.addQueryItem("StartTimeTicks", QString::number(m_resumePosition));
|
params.addQueryItem("StartTimeTicks", QString::number(m_resumePosition));
|
||||||
params.addQueryItem("IsPlayback", "true");
|
params.addQueryItem("IsPlayback", "true");
|
||||||
params.addQueryItem("AutoOpenLiveStream", this->m_autoOpen ? "true" : "false");
|
params.addQueryItem("AutoOpenLiveStream", autoOpen? "true" : "false");
|
||||||
params.addQueryItem("MediaSourceId", this->m_item->jellyfinId());
|
params.addQueryItem("MediaSourceId", item->jellyfinId());
|
||||||
params.addQueryItem("SubtitleStreamIndex", QString::number(m_subtitleIndex));
|
params.addQueryItem("SubtitleStreamIndex", QString::number(m_subtitleIndex));
|
||||||
params.addQueryItem("AudioStreamIndex", QString::number(m_audioIndex));
|
params.addQueryItem("AudioStreamIndex", QString::number(m_audioIndex));
|
||||||
|
|
||||||
QJsonObject root;
|
QJsonObject root;
|
||||||
root["DeviceProfile"] = m_apiClient->playbackDeviceProfile();
|
root["DeviceProfile"] = m_apiClient->playbackDeviceProfile();
|
||||||
|
|
||||||
QNetworkReply *rep = m_apiClient->post("/Items/" + this->m_item->jellyfinId() + "/PlaybackInfo", QJsonDocument(root), params);
|
QNetworkReply *rep = m_apiClient->post("/Items/" + item->jellyfinId() + "/PlaybackInfo", QJsonDocument(root), params);
|
||||||
connect(rep, &QNetworkReply::finished, this, [this, rep]() {
|
connect(rep, &QNetworkReply::finished, this, [this, rep, callback]() {
|
||||||
QJsonObject root = QJsonDocument::fromJson(rep->readAll()).object();
|
QJsonObject root = QJsonDocument::fromJson(rep->readAll()).object();
|
||||||
this->m_playSessionId = root["PlaySessionId"].toString();
|
this->m_playSessionId = root["PlaySessionId"].toString();
|
||||||
qDebug() << "Session id: " << this->m_playSessionId;
|
qDebug() << "Session id: " << this->m_playSessionId;
|
||||||
|
@ -81,25 +85,35 @@ void PlaybackManager::fetchStreamUrl() {
|
||||||
}
|
}
|
||||||
QString streamUrl = this->m_apiClient->baseUrl() + "/" + mediaType + "/" + m_item->jellyfinId() + "/stream."
|
QString streamUrl = this->m_apiClient->baseUrl() + "/" + mediaType + "/" + m_item->jellyfinId() + "/stream."
|
||||||
+ firstMediaSource["Container"].toString() + "?" + query.toString(QUrl::EncodeReserved);
|
+ firstMediaSource["Container"].toString() + "?" + query.toString(QUrl::EncodeReserved);
|
||||||
setStreamUrl(streamUrl);
|
callback(QUrl(streamUrl), DirectPlay);
|
||||||
this->m_playMethod = DirectPlay;
|
|
||||||
} else if (firstMediaSource["SupportsTranscoding"].toBool() && !firstMediaSource["TranscodingUrl"].isNull()) {
|
} else if (firstMediaSource["SupportsTranscoding"].toBool() && !firstMediaSource["TranscodingUrl"].isNull()) {
|
||||||
QString streamUrl = this->m_apiClient->baseUrl()
|
QString streamUrl = this->m_apiClient->baseUrl()
|
||||||
+ firstMediaSource["TranscodingUrl"].toString();
|
+ firstMediaSource["TranscodingUrl"].toString();
|
||||||
|
|
||||||
this->m_playMethod = Transcode;
|
this->m_playMethod = Transcode;
|
||||||
setStreamUrl(streamUrl);
|
callback(QUrl(streamUrl), Transcode);
|
||||||
} else {
|
} else {
|
||||||
qDebug() << "No stream url found";
|
qDebug() << "No stream url found";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
qDebug() << "Found stream url: " << this->m_streamUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rep->deleteLater();
|
rep->deleteLater();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlaybackManager::fetchAndSetStreamUrl(const Item *item) {
|
||||||
|
fetchStreamUrl(item, m_autoOpen, [this, item](QUrl &&url, PlayMethod playbackMethod) {
|
||||||
|
if (m_item == item) {
|
||||||
|
setStreamUrl(url.toString());
|
||||||
|
m_playMethod = playbackMethod;
|
||||||
|
emit playMethodChanged(m_playMethod);
|
||||||
|
m_mediaPlayer->setMedia(QMediaContent(url));
|
||||||
|
m_mediaPlayer->play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
void PlaybackManager::setItem(Item *newItem) {
|
void PlaybackManager::setItem(Item *newItem) {
|
||||||
if (m_mediaPlayer != nullptr) m_mediaPlayer->stop();
|
if (m_mediaPlayer != nullptr) m_mediaPlayer->stop();
|
||||||
|
|
||||||
|
@ -109,8 +123,6 @@ void PlaybackManager::setItem(Item *newItem) {
|
||||||
}
|
}
|
||||||
this->m_item = newItem;
|
this->m_item = newItem;
|
||||||
emit itemChanged(newItem);
|
emit itemChanged(newItem);
|
||||||
// Don't try to start fetching when we're not completely parsed yet.
|
|
||||||
if (m_qmlIsParsingComponent) return;
|
|
||||||
|
|
||||||
if (m_apiClient == nullptr) {
|
if (m_apiClient == nullptr) {
|
||||||
qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
|
qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
|
||||||
|
@ -130,27 +142,33 @@ void PlaybackManager::setItem(Item *newItem) {
|
||||||
newItem->setParent(this);
|
newItem->setParent(this);
|
||||||
}
|
}
|
||||||
if (m_item->status() == RemoteData::Ready) {
|
if (m_item->status() == RemoteData::Ready) {
|
||||||
fetchStreamUrl();
|
fetchAndSetStreamUrl(m_item);
|
||||||
} else {
|
} else {
|
||||||
connect(m_item, &RemoteData::ready, [this]() -> void {
|
connect(m_item, &RemoteData::ready, [this]() -> void {
|
||||||
fetchStreamUrl();
|
fetchAndSetStreamUrl(m_item);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void PlaybackManager::setStreamUrl(const QString &streamUrl) {
|
void PlaybackManager::setStreamUrl(const QString &streamUrl) {
|
||||||
this->m_streamUrl = streamUrl;
|
this->m_streamUrl = streamUrl;
|
||||||
// Inspired by PHP naming schemes
|
// Inspired by PHP naming schemes
|
||||||
QUrl realStreamUrl(streamUrl);
|
QUrl realStreamUrl(streamUrl);
|
||||||
Q_ASSERT_X(realStreamUrl.isValid(), "setStreamUrl", "StreamURL Jellyfin returned is not valid");
|
Q_ASSERT_X(realStreamUrl.isValid(), "setStreamUrl", "StreamURL Jellyfin returned is not valid");
|
||||||
if (m_mediaPlayer != nullptr) {
|
|
||||||
m_mediaPlayer->setMedia(QMediaContent(realStreamUrl));
|
|
||||||
}
|
|
||||||
emit streamUrlChanged(streamUrl);
|
emit streamUrlChanged(streamUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlaybackManager::setPlaybackState(QMediaPlayer::State newState) {
|
||||||
|
if (newState != m_playbackState) {
|
||||||
|
m_playbackState = newState;
|
||||||
|
emit playbackStateChanged(newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void PlaybackManager::mediaPlayerPositionChanged(qint64 position) {
|
void PlaybackManager::mediaPlayerPositionChanged(qint64 position) {
|
||||||
|
emit positionChanged(position);
|
||||||
if (position == 0 && m_oldPosition != 0) {
|
if (position == 0 && m_oldPosition != 0) {
|
||||||
// Save the old position when stop gets called. The QMediaPlayer will try to set
|
// Save the old position when stop gets called. The QMediaPlayer will try to set
|
||||||
// position to 0 when stopped, but we don't want to report that to Jellyfin. We
|
// position to 0 when stopped, but we don't want to report that to Jellyfin. We
|
||||||
|
@ -171,6 +189,7 @@ void PlaybackManager::mediaPlayerStateChanged(QMediaPlayer::State newState) {
|
||||||
// We've stopped playing the media. Post a stop signal.
|
// We've stopped playing the media. Post a stop signal.
|
||||||
m_updateTimer.stop();
|
m_updateTimer.stop();
|
||||||
postPlaybackInfo(Stopped);
|
postPlaybackInfo(Stopped);
|
||||||
|
setPlaybackState(QMediaPlayer::StoppedState);
|
||||||
} else {
|
} else {
|
||||||
postPlaybackInfo(Progress);
|
postPlaybackInfo(Progress);
|
||||||
}
|
}
|
||||||
|
@ -178,8 +197,10 @@ void PlaybackManager::mediaPlayerStateChanged(QMediaPlayer::State newState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus) {
|
void PlaybackManager::mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus) {
|
||||||
|
emit mediaStatusChanged(newStatus);
|
||||||
if (newStatus == QMediaPlayer::LoadedMedia) {
|
if (newStatus == QMediaPlayer::LoadedMedia) {
|
||||||
m_mediaPlayer->play();
|
m_mediaPlayer->play();
|
||||||
|
setPlaybackState(playbackState());
|
||||||
if (m_resumePlayback) {
|
if (m_resumePlayback) {
|
||||||
qDebug() << "Resuming playback by seeking to " << (m_resumePosition / MS_TICK_FACTOR);
|
qDebug() << "Resuming playback by seeking to " << (m_resumePosition / MS_TICK_FACTOR);
|
||||||
m_mediaPlayer->setPosition(m_resumePosition / MS_TICK_FACTOR);
|
m_mediaPlayer->setPosition(m_resumePosition / MS_TICK_FACTOR);
|
||||||
|
@ -187,24 +208,9 @@ void PlaybackManager::mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus ne
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::setMediaPlayer(QObject *qmlMediaPlayer) {
|
void PlaybackManager::mediaPlayerError(QMediaPlayer::Error error) {
|
||||||
if (m_mediaPlayer != nullptr) {
|
emit errorChanged(error);
|
||||||
// Clean up the old media player.
|
emit errorStringChanged(m_mediaPlayer->errorString());
|
||||||
disconnect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
|
|
||||||
disconnect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
|
|
||||||
disconnect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_qmlMediaPlayer = qmlMediaPlayer;
|
|
||||||
if (qmlMediaPlayer != nullptr) {
|
|
||||||
m_mediaPlayer = qvariant_cast<QMediaPlayer *>(qmlMediaPlayer->property("mediaObject"));
|
|
||||||
Q_ASSERT_X(m_mediaPlayer != nullptr, "setMediaPlayer", "The mediaPlayer property must contain a qml MediaPlayer with the mediaObject property");
|
|
||||||
|
|
||||||
// Connect signals from the new media player
|
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
|
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
|
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::updatePlaybackInfo() {
|
void PlaybackManager::updatePlaybackInfo() {
|
||||||
|
@ -213,7 +219,34 @@ void PlaybackManager::updatePlaybackInfo() {
|
||||||
|
|
||||||
void PlaybackManager::playItem(const QString &itemId) {
|
void PlaybackManager::playItem(const QString &itemId) {
|
||||||
Item *newItem = new Item(itemId, m_apiClient, this);
|
Item *newItem = new Item(itemId, m_apiClient, this);
|
||||||
|
QString parentId = newItem->parentId();
|
||||||
setItem(newItem);
|
setItem(newItem);
|
||||||
|
ItemModel *queue = new UserItemModel(this);
|
||||||
|
setQueue(queue);
|
||||||
|
connect(newItem, &Item::ready, this, [this, queue, parentId](){
|
||||||
|
queue->setParentId(parentId);
|
||||||
|
queue->setLimit(10000);
|
||||||
|
queue->setApiClient(m_apiClient);
|
||||||
|
queue->reload();
|
||||||
|
});
|
||||||
|
connect(queue, &BaseApiModel::ready, this, [this, queue, newItem]() {
|
||||||
|
for (int i = 0; i < queue->size(); i++) {
|
||||||
|
if (queue->at(i)->jellyfinId() == newItem->jellyfinId()) {
|
||||||
|
m_queueIndex = i;
|
||||||
|
emit queueIndexChanged(m_queueIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setPlaybackState(QMediaPlayer::PlayingState);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaybackManager::playItemInList(ItemModel *playlist, int itemIdx) {
|
||||||
|
playlist->setParent(this);
|
||||||
|
setQueue(playlist);
|
||||||
|
m_queueIndex = itemIdx;
|
||||||
|
emit queueIndexChanged(m_queueIndex);
|
||||||
|
setItem(playlist->at(itemIdx));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::next() {
|
void PlaybackManager::next() {
|
||||||
|
@ -224,6 +257,7 @@ void PlaybackManager::previous() {
|
||||||
Q_UNIMPLEMENTED();
|
Q_UNIMPLEMENTED();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
|
void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
|
||||||
QJsonObject root;
|
QJsonObject root;
|
||||||
|
|
||||||
|
@ -272,15 +306,62 @@ void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
|
||||||
m_apiClient->setDefaultErrorHandler(rep);
|
m_apiClient->setDefaultErrorHandler(rep);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlaybackManager::swapMediaPlayer() {
|
||||||
|
if (m_mediaPlayer != nullptr) {
|
||||||
|
disconnect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
|
||||||
|
disconnect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
|
||||||
|
disconnect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::durationChanged);
|
||||||
|
disconnect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
|
||||||
|
disconnect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged);
|
||||||
|
// I do not like the complicated overload cast
|
||||||
|
disconnect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::error)), this, SLOT(mediaPlayerError(QmediaPlayer::error)));
|
||||||
|
}
|
||||||
|
if (m_mediaPlayer == m_mediaPlayer1) {
|
||||||
|
m_mediaPlayer = m_mediaPlayer2;
|
||||||
|
emit mediaPlayerChanged(m_mediaPlayer);
|
||||||
|
} else {
|
||||||
|
m_mediaPlayer = m_mediaPlayer1;
|
||||||
|
emit mediaPlayerChanged(m_mediaPlayer);
|
||||||
|
}
|
||||||
|
connect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
|
||||||
|
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
|
||||||
|
connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::durationChanged);
|
||||||
|
connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
|
||||||
|
connect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged);
|
||||||
|
// I do not like the complicated overload cast
|
||||||
|
connect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::error)), this, SLOT(mediaPlayerError(QmediaPlayer::error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Item *PlaybackManager::nextItem() {
|
||||||
|
if (m_queue == nullptr) return nullptr;
|
||||||
|
// TODO: shuffle etc.
|
||||||
|
if (m_queueIndex < m_queue->size()) {
|
||||||
|
return m_queue->at(m_queueIndex + 1);
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaybackManager::setQueue(ItemModel *model) {
|
||||||
|
if (m_queue != nullptr) {
|
||||||
|
if (QQmlEngine::objectOwnership(m_queue) == QQmlEngine::CppOwnership) {
|
||||||
|
m_queue->deleteLater();
|
||||||
|
} else {
|
||||||
|
m_queue->setParent(nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_queue = model;
|
||||||
|
emit queueChanged(m_queue);
|
||||||
|
}
|
||||||
|
|
||||||
void PlaybackManager::componentComplete() {
|
void PlaybackManager::componentComplete() {
|
||||||
if (m_apiClient == nullptr) qWarning() << "No ApiClient set for PlaybackManager";
|
if (m_apiClient == nullptr) qWarning() << "No ApiClient set for PlaybackManager";
|
||||||
m_qmlIsParsingComponent = false;
|
m_qmlIsParsingComponent = false;
|
||||||
if (m_item != nullptr) {
|
if (m_item != nullptr) {
|
||||||
if (m_item->status() == RemoteData::Ready) {
|
if (m_item->status() == RemoteData::Ready) {
|
||||||
fetchStreamUrl();
|
fetchAndSetStreamUrl(m_item);
|
||||||
} else {
|
} else {
|
||||||
connect(m_item, &RemoteData::ready, [this]() -> void {
|
connect(m_item, &RemoteData::ready, [this]() -> void {
|
||||||
fetchStreamUrl();
|
fetchAndSetStreamUrl(m_item);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,9 +105,11 @@ void WebSocket::textMessageReceived(const QString &message) {
|
||||||
}
|
}
|
||||||
QJsonArray userDataList = data2["UserDataList"].toArray();
|
QJsonArray userDataList = data2["UserDataList"].toArray();
|
||||||
for (QJsonValue val: userDataList) {
|
for (QJsonValue val: userDataList) {
|
||||||
QSharedPointer<DTO::UserData> userData(new DTO::UserData, &QObject::deleteLater);
|
UserData* userData =new DTO::UserData;
|
||||||
userData->deserialize(val.toObject());
|
userData->deserialize(val.toObject());
|
||||||
|
userData->setParent(this);
|
||||||
m_apiClient->onUserDataChanged(userData->itemId(), userData);
|
m_apiClient->onUserDataChanged(userData->itemId(), userData);
|
||||||
|
userData->deleteLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ set(sailfin_QML_SOURCES
|
||||||
qml/components/MoreSection.qml
|
qml/components/MoreSection.qml
|
||||||
qml/components/PlainLabel.qml
|
qml/components/PlainLabel.qml
|
||||||
qml/components/PlaybackBar.qml
|
qml/components/PlaybackBar.qml
|
||||||
|
qml/components/PlayQueue.qml
|
||||||
qml/components/PlayToolbar.qml
|
qml/components/PlayToolbar.qml
|
||||||
qml/components/RemoteImage.qml
|
qml/components/RemoteImage.qml
|
||||||
qml/components/Shim.qml
|
qml/components/Shim.qml
|
||||||
|
@ -65,6 +66,8 @@ target_link_libraries(harbour-sailfin PRIVATE Qt5::Gui Qt5::Qml Qt5::Quick Sailf
|
||||||
# Note: this may break when the compiler changes. -rdynamic and -pie seem to be needed for the
|
# Note: this may break when the compiler changes. -rdynamic and -pie seem to be needed for the
|
||||||
# invoker/booster to work
|
# invoker/booster to work
|
||||||
jellyfin-qt "-Wl,-rpath,${CMAKE_INSTALL_LIBDIR} -rdynamic -pie")
|
jellyfin-qt "-Wl,-rpath,${CMAKE_INSTALL_LIBDIR} -rdynamic -pie")
|
||||||
|
target_compile_definitions(harbour-sailfin
|
||||||
|
PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>)
|
||||||
|
|
||||||
install(TARGETS harbour-sailfin
|
install(TARGETS harbour-sailfin
|
||||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||||
|
@ -107,5 +110,5 @@ install(FILES icons/172x172/harbour-sailfin.png
|
||||||
# format.
|
# format.
|
||||||
file(WRITE "${CMAKE_BINARY_DIR}/QtCreatorDeployment.txt"
|
file(WRITE "${CMAKE_BINARY_DIR}/QtCreatorDeployment.txt"
|
||||||
"${CMAKE_INSTALL_PREFIX}
|
"${CMAKE_INSTALL_PREFIX}
|
||||||
sailfish/harbour-sailfin:bin
|
${CMAKE_BINARY_DIR}/sailfish/harbour-sailfin:bin
|
||||||
")
|
")
|
||||||
|
|
|
@ -48,7 +48,7 @@ function ticksToText(ticks, showHours) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function itemImageUrl(baseUrl, item, type, options) {
|
function itemImageUrl(baseUrl, item, type, options) {
|
||||||
if (!item.imageTags[type]) { return "" }
|
if (item === null || !item.imageTags[type]) { return "" }
|
||||||
return itemModelImageUrl(baseUrl, item.jellyfinId, item.imageTags[type], type, options)
|
return itemModelImageUrl(baseUrl, item.jellyfinId, item.imageTags[type], type, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
sailfish/qml/components/PlayQueue.qml
Normal file
16
sailfish/qml/components/PlayQueue.qml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import QtQuick 2.6
|
||||||
|
import Sailfish.Silica 1.0
|
||||||
|
|
||||||
|
import nl.netsoj.chris.Jellyfin 1.0
|
||||||
|
|
||||||
|
import "music"
|
||||||
|
|
||||||
|
SilicaListView {
|
||||||
|
header: SectionHeader { text: qsTr("Play queue") }
|
||||||
|
delegate: SongDelegate {
|
||||||
|
artists: model.artists
|
||||||
|
name: model.name
|
||||||
|
width: parent.width
|
||||||
|
indexNumber: ListView.index
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,6 +43,8 @@ PanelBackground {
|
||||||
property PlaybackManager manager
|
property PlaybackManager manager
|
||||||
property bool open
|
property bool open
|
||||||
property real visibleSize: height
|
property real visibleSize: height
|
||||||
|
property bool isFullPage: false
|
||||||
|
property bool showQueue: false
|
||||||
|
|
||||||
property bool _pageWasShowingNavigationIndicator
|
property bool _pageWasShowingNavigationIndicator
|
||||||
|
|
||||||
|
@ -53,7 +55,7 @@ PanelBackground {
|
||||||
id: backgroundItem
|
id: backgroundItem
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height
|
height: parent.height
|
||||||
onClicked: playbackBar.state = (playbackBar.state == "large" ? "open" : "large")
|
onClicked: playbackBar.state = "large"
|
||||||
|
|
||||||
|
|
||||||
RemoteImage {
|
RemoteImage {
|
||||||
|
@ -64,7 +66,10 @@ PanelBackground {
|
||||||
top: parent.top
|
top: parent.top
|
||||||
}
|
}
|
||||||
width: height
|
width: height
|
||||||
blurhash: manager.item.imageBlurHashes["Primary"][manager.item.imageTags["Primary"]]
|
Binding on blurhash {
|
||||||
|
when: manager.item !== null && "Primary" in manager.item.imageBlurHashes
|
||||||
|
value: manager.item.imageBlurHashes["Primary"][manager.item.imageTags["Primary"]]
|
||||||
|
}
|
||||||
source: largeAlbumArt.source
|
source: largeAlbumArt.source
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
|
||||||
|
@ -77,6 +82,20 @@ PanelBackground {
|
||||||
Behavior on opacity { FadeAnimation {} }
|
Behavior on opacity { FadeAnimation {} }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loader {
|
||||||
|
id: queueLoader
|
||||||
|
source: Qt.resolvedUrl("PlayQueue.qml")
|
||||||
|
anchors.fill: albumArt
|
||||||
|
active: false
|
||||||
|
visible: false
|
||||||
|
Binding {
|
||||||
|
when: queueLoader.item !== null
|
||||||
|
target: queueLoader.item
|
||||||
|
property: "model"
|
||||||
|
value: manager.queue
|
||||||
|
//currentIndex: manager.queueIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: artistInfo
|
id: artistInfo
|
||||||
|
@ -106,6 +125,7 @@ PanelBackground {
|
||||||
case "Audio":
|
case "Audio":
|
||||||
return manager.item.artists.join(", ")
|
return manager.item.artists.join(", ")
|
||||||
}
|
}
|
||||||
|
return qsTr("Not audio")
|
||||||
}
|
}
|
||||||
width: Math.min(contentWidth, parent.width)
|
width: Math.min(contentWidth, parent.width)
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
@ -146,11 +166,11 @@ PanelBackground {
|
||||||
rightMargin: Theme.paddingMedium
|
rightMargin: Theme.paddingMedium
|
||||||
verticalCenter: parent.verticalCenter
|
verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
icon.source: appWindow.mediaPlayer.playbackState === MediaPlayer.PlayingState
|
icon.source: manager.playbackState === MediaPlayer.PlayingState
|
||||||
? "image://theme/icon-m-pause" : "image://theme/icon-m-play"
|
? "image://theme/icon-m-pause" : "image://theme/icon-m-play"
|
||||||
onClicked: appWindow.mediaPlayer.playbackState === MediaPlayer.PlayingState
|
onClicked: manager.playbackState === MediaPlayer.PlayingState
|
||||||
? appWindow.mediaPlayer.pause()
|
? manager.pause()
|
||||||
: appWindow.mediaPlayer.play()
|
: manager.play()
|
||||||
}
|
}
|
||||||
IconButton {
|
IconButton {
|
||||||
id: nextButton
|
id: nextButton
|
||||||
|
@ -171,8 +191,10 @@ PanelBackground {
|
||||||
verticalCenter: playButton.verticalCenter
|
verticalCenter: playButton.verticalCenter
|
||||||
}
|
}
|
||||||
icon.source: "image://theme/icon-m-menu"
|
icon.source: "image://theme/icon-m-menu"
|
||||||
|
icon.highlighted: showQueue
|
||||||
enabled: false
|
enabled: false
|
||||||
opacity: 0
|
opacity: 0
|
||||||
|
onClicked: showQueue = !showQueue
|
||||||
}
|
}
|
||||||
|
|
||||||
ProgressBar {
|
ProgressBar {
|
||||||
|
@ -182,9 +204,9 @@ PanelBackground {
|
||||||
leftMargin: Theme.itemSizeLarge
|
leftMargin: Theme.itemSizeLarge
|
||||||
rightMargin: 0
|
rightMargin: 0
|
||||||
minimumValue: 0
|
minimumValue: 0
|
||||||
value: appWindow.mediaPlayer.position
|
value: manager.position
|
||||||
maximumValue: appWindow.mediaPlayer.duration
|
maximumValue: manager.duration
|
||||||
indeterminate: [MediaPlayer.Loading, MediaPlayer.Buffering].indexOf(appWindow.mediaPlayer.status) >= 0
|
indeterminate: [MediaPlayer.Loading, MediaPlayer.Buffering].indexOf(manager.mediaStatus) >= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
Slider {
|
Slider {
|
||||||
|
@ -192,17 +214,17 @@ PanelBackground {
|
||||||
animateValue: false
|
animateValue: false
|
||||||
anchors.verticalCenter: progressBar.top
|
anchors.verticalCenter: progressBar.top
|
||||||
minimumValue: 0
|
minimumValue: 0
|
||||||
value: appWindow.mediaPlayer.position
|
value: manager.position
|
||||||
maximumValue: appWindow.mediaPlayer.duration
|
maximumValue: manager.duration
|
||||||
width: parent.width
|
width: parent.width
|
||||||
stepSize: 1000
|
stepSize: 1000
|
||||||
valueText: Utils.timeToText(value)
|
valueText: Utils.timeToText(value)
|
||||||
enabled: false
|
enabled: false
|
||||||
visible: false
|
visible: false
|
||||||
onDownChanged: { if (!down) {
|
onDownChanged: { if (!down) {
|
||||||
appWindow.mediaPlayer.seek(value);
|
manager.seek(value);
|
||||||
// For some reason, the binding breaks when dragging the slider.
|
// For some reason, the binding breaks when dragging the slider.
|
||||||
value = Qt.binding(function() { return appWindow.mediaPlayer.position})
|
value = Qt.binding(function() { return manager.position})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -212,7 +234,7 @@ PanelBackground {
|
||||||
states: [
|
states: [
|
||||||
State {
|
State {
|
||||||
name: ""
|
name: ""
|
||||||
when: appWindow.mediaPlayer.playbackState !== MediaPlayer.StoppedState && state != "page" && !("__hidePlaybackBar" in pageStack.currentPage)
|
when: manager.playbackState !== MediaPlayer.StoppedState && !isFullPage && !("__hidePlaybackBar" in pageStack.currentPage)
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "large"
|
name: "large"
|
||||||
|
@ -330,7 +352,7 @@ PanelBackground {
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "hidden"
|
name: "hidden"
|
||||||
when: (appWindow.mediaPlayer.playbackState === MediaPlayer.StoppedState || "__hidePlaybackBar" in pageStack.currentPage) && state != "page"
|
when: (manager.playbackState === MediaPlayer.StoppedState || "__hidePlaybackBar" in pageStack.currentPage) && !isFullPage
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: playbackBarTranslate
|
target: playbackBarTranslate
|
||||||
// + small padding since the ProgressBar otherwise would stick out
|
// + small padding since the ProgressBar otherwise would stick out
|
||||||
|
@ -347,7 +369,26 @@ PanelBackground {
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "page"
|
name: "page"
|
||||||
|
when: isFullPage && !showQueue
|
||||||
extend: "large"
|
extend: "large"
|
||||||
|
PropertyChanges {
|
||||||
|
target: queueLoader
|
||||||
|
active: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "pageQueue"
|
||||||
|
when: isFullPage && showQueue
|
||||||
|
extend: "page"
|
||||||
|
PropertyChanges {
|
||||||
|
target: queueLoader
|
||||||
|
visible: true
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: largeAlbumArt
|
||||||
|
opacity: 0
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -371,7 +412,7 @@ PanelBackground {
|
||||||
}
|
}
|
||||||
Loader {
|
Loader {
|
||||||
Component.onCompleted: setSource(Qt.resolvedUrl("PlaybackBar.qml"),
|
Component.onCompleted: setSource(Qt.resolvedUrl("PlaybackBar.qml"),
|
||||||
{"state": "page", "manager": manager, "y": 0})
|
{"isFullPage": true, "manager": manager, "y": 0})
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -421,8 +462,6 @@ PanelBackground {
|
||||||
},
|
},
|
||||||
Transition {
|
Transition {
|
||||||
from: "hidden"
|
from: "hidden"
|
||||||
SequentialAnimation {
|
|
||||||
ParallelAnimation {
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
targets: [playbackBarTranslate, playbackBar]
|
targets: [playbackBarTranslate, playbackBar]
|
||||||
properties: "y,visibileSize"
|
properties: "y,visibileSize"
|
||||||
|
@ -437,8 +476,6 @@ PanelBackground {
|
||||||
to: Theme.itemSizeLarge
|
to: Theme.itemSizeLarge
|
||||||
easing.type: Easing.OutQuad
|
easing.type: Easing.OutQuad
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Transition {
|
Transition {
|
||||||
from: ""
|
from: ""
|
||||||
|
|
|
@ -36,10 +36,10 @@ SilicaItem {
|
||||||
property bool resume
|
property bool resume
|
||||||
property int progress
|
property int progress
|
||||||
readonly property bool landscape: videoOutput.contentRect.width > videoOutput.contentRect.height
|
readonly property bool landscape: videoOutput.contentRect.width > videoOutput.contentRect.height
|
||||||
property MediaPlayer player
|
readonly property bool hudVisible: !hud.hidden || manager.error !== MediaPlayer.NoError
|
||||||
readonly property bool hudVisible: !hud.hidden || player.error !== MediaPlayer.NoError
|
|
||||||
property int audioTrack: 0
|
property int audioTrack: 0
|
||||||
property int subtitleTrack: 0
|
property int subtitleTrack: 0
|
||||||
|
property PlaybackManager manager;
|
||||||
|
|
||||||
// Blackground to prevent the ambience from leaking through
|
// Blackground to prevent the ambience from leaking through
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
@ -49,27 +49,27 @@ SilicaItem {
|
||||||
|
|
||||||
VideoOutput {
|
VideoOutput {
|
||||||
id: videoOutput
|
id: videoOutput
|
||||||
source: player
|
source: manager
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
}
|
}
|
||||||
|
|
||||||
VideoHud {
|
VideoHud {
|
||||||
id: hud
|
id: hud
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
player: playerRoot.player
|
manager: playerRoot.manager
|
||||||
title: videoPlayer.title
|
title: videoPlayer.title
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.horizontalPageMargin
|
anchors.margins: Theme.horizontalPageMargin
|
||||||
text: item.jellyfinId + "\n" + appWindow.playbackManager.streamUrl + "\n"
|
text: item.jellyfinId + "\n" + appWindow.playbackManager.streamUrl + "\n"
|
||||||
+ (appWindow.playbackManager.playMethod == PlaybackManager.DirectPlay ? "Direct Play" : "Transcoding") + "\n"
|
+ (manager.playMethod === PlaybackManager.DirectPlay ? "Direct Play" : "Transcoding") + "\n"
|
||||||
+ player.position + "\n"
|
+ manager.position + "\n"
|
||||||
+ player.status + "\n"
|
+ manager.mediaStatus + "\n"
|
||||||
+ player.bufferProgress + "\n"
|
// + player.bufferProgress + "\n"
|
||||||
+ player.metaData.videoCodec + "@" + player.metaData.videoFrameRate + "(" + player.metaData.videoBitRate + ")" + "\n"
|
// + player.metaData.videoCodec + "@" + player.metaData.videoFrameRate + "(" + player.metaData.videoBitRate + ")" + "\n"
|
||||||
+ player.metaData.audioCodec + "(" + player.metaData.audioBitRate + ")" + "\n"
|
// + player.metaData.audioCodec + "(" + player.metaData.audioBitRate + ")" + "\n"
|
||||||
+ player.errorString + "\n"
|
// + player.errorString + "\n"
|
||||||
font.pixelSize: Theme.fontSizeExtraSmall
|
font.pixelSize: Theme.fontSizeExtraSmall
|
||||||
wrapMode: "WordWrap"
|
wrapMode: "WordWrap"
|
||||||
visible: appWindow.showDebugInfo
|
visible: appWindow.showDebugInfo
|
||||||
|
@ -78,17 +78,17 @@ SilicaItem {
|
||||||
|
|
||||||
VideoError {
|
VideoError {
|
||||||
anchors.fill: videoOutput
|
anchors.fill: videoOutput
|
||||||
player: playerRoot.player
|
player: manager
|
||||||
}
|
}
|
||||||
|
|
||||||
function start() {
|
function start() {
|
||||||
appWindow.playbackManager.audioIndex = audioTrack
|
manager.audioIndex = audioTrack
|
||||||
appWindow.playbackManager.subtitleIndex = subtitleTrack
|
manager.subtitleIndex = subtitleTrack
|
||||||
appWindow.playbackManager.resumePlayback = resume
|
manager.resumePlayback = resume
|
||||||
appWindow.playbackManager.item = item
|
manager.playItem(item.jellyfinId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop() {
|
function stop() {
|
||||||
player.stop()
|
manager.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,11 @@ import QtQuick 2.6
|
||||||
import Sailfish.Silica 1.0
|
import Sailfish.Silica 1.0
|
||||||
import QtMultimedia 5.6
|
import QtMultimedia 5.6
|
||||||
|
|
||||||
|
import nl.netsoj.chris.Jellyfin 1.0
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: videoError
|
id: videoError
|
||||||
property MediaPlayer player
|
property PlaybackManager player
|
||||||
color: pal.palette.overlayBackgroundColor
|
color: pal.palette.overlayBackgroundColor
|
||||||
opacity: player.error === MediaPlayer.NoError ? 0.0 : 1.0
|
opacity: player.error === MediaPlayer.NoError ? 0.0 : 1.0
|
||||||
Behavior on opacity { FadeAnimator {} }
|
Behavior on opacity { FadeAnimator {} }
|
||||||
|
|
|
@ -20,6 +20,8 @@ import QtQuick 2.6
|
||||||
import QtMultimedia 5.6
|
import QtMultimedia 5.6
|
||||||
import Sailfish.Silica 1.0
|
import Sailfish.Silica 1.0
|
||||||
|
|
||||||
|
import nl.netsoj.chris.Jellyfin 1.0
|
||||||
|
|
||||||
import "../../Utils.js" as Utils
|
import "../../Utils.js" as Utils
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,7 +30,7 @@ import "../../Utils.js" as Utils
|
||||||
*/
|
*/
|
||||||
Item {
|
Item {
|
||||||
id: videoHud
|
id: videoHud
|
||||||
property MediaPlayer player
|
property PlaybackManager manager
|
||||||
property string title
|
property string title
|
||||||
property bool _manuallyActivated: false
|
property bool _manuallyActivated: false
|
||||||
readonly property bool hidden: opacity == 0.0
|
readonly property bool hidden: opacity == 0.0
|
||||||
|
@ -76,19 +78,19 @@ Item {
|
||||||
id: busyIndicator
|
id: busyIndicator
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
size: BusyIndicatorSize.Medium
|
size: BusyIndicatorSize.Medium
|
||||||
running: [MediaPlayer.Loading, MediaPlayer.Stalled].indexOf(player.status) >= 0
|
running: [MediaPlayer.Loading, MediaPlayer.Stalled].indexOf(manager.mediaStatus) >= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton {
|
IconButton {
|
||||||
id: playPause
|
id: playPause
|
||||||
enabled: !hidden
|
enabled: !hidden
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
icon.source: player.playbackState == MediaPlayer.PausedState ? "image://theme/icon-l-play" : "image://theme/icon-l-pause"
|
icon.source: manager.playbackState === MediaPlayer.PausedState ? "image://theme/icon-l-play" : "image://theme/icon-l-pause"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (player.playbackState == MediaPlayer.PlayingState) {
|
if (manager.playbackState === MediaPlayer.PlayingState) {
|
||||||
player.pause()
|
manager.pause()
|
||||||
} else {
|
} else {
|
||||||
player.play()
|
manager.play()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
visible: !busyIndicator.running
|
visible: !busyIndicator.running
|
||||||
|
@ -99,7 +101,7 @@ Item {
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: progress.height
|
height: progress.height
|
||||||
visible: [MediaPlayer.Unavailable, MediaPlayer.Loading, MediaPlayer.NoMedia].indexOf(player.status) == -1
|
visible: [MediaPlayer.Unavailable, MediaPlayer.Loading, MediaPlayer.NoMedia].indexOf(manager.mediaStatus) == -1
|
||||||
|
|
||||||
gradient: Gradient {
|
gradient: Gradient {
|
||||||
GradientStop { position: 0.0; color: Theme.rgba(palette.overlayBackgroundColor, 0.15); }
|
GradientStop { position: 0.0; color: Theme.rgba(palette.overlayBackgroundColor, 0.15); }
|
||||||
|
@ -116,19 +118,19 @@ Item {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: Theme.horizontalPageMargin
|
anchors.leftMargin: Theme.horizontalPageMargin
|
||||||
anchors.verticalCenter: progressSlider.verticalCenter
|
anchors.verticalCenter: progressSlider.verticalCenter
|
||||||
text: Utils.timeToText(player.position)
|
text: Utils.timeToText(manager.position)
|
||||||
}
|
}
|
||||||
|
|
||||||
Slider {
|
Slider {
|
||||||
id: progressSlider
|
id: progressSlider
|
||||||
enabled: player.seekable
|
enabled: manager.seekable
|
||||||
value: player.position
|
value: manager.position
|
||||||
maximumValue: player.duration
|
maximumValue: manager.duration
|
||||||
stepSize: 1000
|
stepSize: 1000
|
||||||
anchors.left: playedTime.right
|
anchors.left: playedTime.right
|
||||||
anchors.right: totalTime.left
|
anchors.right: totalTime.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
onDownChanged: if (!down) { player.seek(value) }
|
onDownChanged: if (!down) { manager.seek(value) }
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
@ -136,7 +138,7 @@ Item {
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.rightMargin: Theme.horizontalPageMargin
|
anchors.rightMargin: Theme.horizontalPageMargin
|
||||||
anchors.verticalCenter: progress.verticalCenter
|
anchors.verticalCenter: progress.verticalCenter
|
||||||
text: Utils.timeToText(player.duration)
|
text: Utils.timeToText(manager.duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,10 +146,10 @@ Item {
|
||||||
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: player
|
target: manager
|
||||||
onStatusChanged: {
|
onMediaStatusChanged: {
|
||||||
console.log("New mediaPlayer status: " + player.status)
|
console.log("New mediaPlayer status: " + manager.mediaStatus)
|
||||||
switch(player.status) {
|
switch(manager.mediaStatus) {
|
||||||
case MediaPlayer.Loaded:
|
case MediaPlayer.Loaded:
|
||||||
case MediaPlayer.Buffering:
|
case MediaPlayer.Buffering:
|
||||||
show(false)
|
show(false)
|
||||||
|
|
|
@ -32,7 +32,7 @@ ApplicationWindow {
|
||||||
id: appWindow
|
id: appWindow
|
||||||
property bool _hasInitialized: false
|
property bool _hasInitialized: false
|
||||||
// The global mediaPlayer instance
|
// The global mediaPlayer instance
|
||||||
readonly property MediaPlayer mediaPlayer: _mediaPlayer
|
//readonly property MediaPlayer mediaPlayer: _mediaPlayer
|
||||||
readonly property PlaybackManager playbackManager: _playbackManager
|
readonly property PlaybackManager playbackManager: _playbackManager
|
||||||
|
|
||||||
// Data of the currently selected item. For use on the cover.
|
// Data of the currently selected item. For use on the cover.
|
||||||
|
@ -41,7 +41,7 @@ ApplicationWindow {
|
||||||
property string collectionId
|
property string collectionId
|
||||||
|
|
||||||
// Bad way to implement settings, but it'll do for now.
|
// Bad way to implement settings, but it'll do for now.
|
||||||
property bool showDebugInfo: false
|
property bool showDebugInfo: true
|
||||||
|
|
||||||
property bool _hidePlaybackBar: false
|
property bool _hidePlaybackBar: false
|
||||||
|
|
||||||
|
@ -65,13 +65,13 @@ ApplicationWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cover: {
|
cover: {
|
||||||
if ([MediaPlayer.NoMedia, MediaPlayer.InvalidMedia, MediaPlayer.UnknownStatus].indexOf(mediaPlayer.status) >= 0) {
|
if ([MediaPlayer.NoMedia, MediaPlayer.InvalidMedia, MediaPlayer.UnknownStatus].indexOf(playbackManager.status) >= 0) {
|
||||||
if (itemData) {
|
if (itemData) {
|
||||||
return Qt.resolvedUrl("cover/PosterCover.qml")
|
return Qt.resolvedUrl("cover/PosterCover.qml")
|
||||||
} else {
|
} else {
|
||||||
return Qt.resolvedUrl("cover/CoverPage.qml")
|
return Qt.resolvedUrl("cover/CoverPage.qml")
|
||||||
}
|
}
|
||||||
} else if (mediaPlayer.hasVideo){
|
} else if (playbackManager.hasVideo){
|
||||||
return Qt.resolvedUrl("cover/VideoCover.qml")
|
return Qt.resolvedUrl("cover/VideoCover.qml")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,26 +87,25 @@ ApplicationWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaPlayer {
|
/*MediaPlayer {
|
||||||
id: _mediaPlayer
|
id: _mediaPlayer
|
||||||
autoPlay: true
|
autoPlay: true
|
||||||
}
|
}*/
|
||||||
|
|
||||||
PlaybackManager {
|
PlaybackManager {
|
||||||
id: _playbackManager
|
id: _playbackManager
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
mediaPlayer: _mediaPlayer
|
|
||||||
audioIndex: 0
|
audioIndex: 0
|
||||||
autoOpen: true
|
autoOpen: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the sytem alive while playing media
|
// Keep the sytem alive while playing media
|
||||||
KeepAlive {
|
KeepAlive {
|
||||||
enabled: _mediaPlayer.playbackState == MediaPlayer.PlayingState
|
enabled: playbackManager.playbackState === MediaPlayer.PlayingState
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplayBlanking {
|
DisplayBlanking {
|
||||||
preventBlanking: _mediaPlayer.playbackState == MediaPlayer.PlayingState && _mediaPlayer.hasVideo
|
preventBlanking: playbackManager.playbackState === MediaPlayer.PlayingState && playbackManager.hasVideo
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackBar {
|
PlaybackBar {
|
||||||
|
|
|
@ -78,7 +78,7 @@ Page {
|
||||||
//- Section header for films and TV shows that an user hasn't completed yet.
|
//- Section header for films and TV shows that an user hasn't completed yet.
|
||||||
text: qsTr("Resume watching")
|
text: qsTr("Resume watching")
|
||||||
clickable: false
|
clickable: false
|
||||||
busy: userResumeModel.status == ApiModel.Loading
|
busy: userResumeModel.status === ApiModel.Loading
|
||||||
Loader {
|
Loader {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
sourceComponent: carrouselView
|
sourceComponent: carrouselView
|
||||||
|
@ -97,7 +97,7 @@ Page {
|
||||||
//- Section header for next episodes in a TV show that an user was watching.
|
//- Section header for next episodes in a TV show that an user was watching.
|
||||||
text: qsTr("Next up")
|
text: qsTr("Next up")
|
||||||
clickable: false
|
clickable: false
|
||||||
busy: showNextUpModel.status == ApiModel.Loading
|
busy: showNextUpModel.status === ApiModel.Loading
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
@ -121,9 +121,9 @@ Page {
|
||||||
model: mediaLibraryModel
|
model: mediaLibraryModel
|
||||||
MoreSection {
|
MoreSection {
|
||||||
text: model.name
|
text: model.name
|
||||||
busy: userItemModel.status != ApiModel.Ready
|
busy: userItemModel.status !== ApiModel.Ready
|
||||||
|
|
||||||
onHeaderClicked: pageStack.push(Qt.resolvedUrl("itemdetails/CollectionPage.qml"), {"itemId": model.id})
|
onHeaderClicked: pageStack.push(Qt.resolvedUrl("itemdetails/CollectionPage.qml"), {"itemId": model.jellyfinId})
|
||||||
Loader {
|
Loader {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
sourceComponent: carrouselView
|
sourceComponent: carrouselView
|
||||||
|
@ -133,16 +133,12 @@ Page {
|
||||||
UserItemLatestModel {
|
UserItemLatestModel {
|
||||||
id: userItemModel
|
id: userItemModel
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
parentId: model.id
|
parentId: jellyfinId
|
||||||
limit: 16
|
limit: 16
|
||||||
}
|
}
|
||||||
Connections {
|
Connections {
|
||||||
target: mediaLibraryModel
|
target: mediaLibraryModel
|
||||||
onStatusChanged: {
|
onReady: userItemModel.reload()
|
||||||
if (status == ApiModel.Ready) {
|
|
||||||
userItemModel.reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,7 +150,6 @@ Page {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: false
|
visible: false
|
||||||
opacity: 0
|
opacity: 0
|
||||||
contentHeight: errorColumn.height
|
|
||||||
|
|
||||||
Loader { sourceComponent: commonPullDownMenu; }
|
Loader { sourceComponent: commonPullDownMenu; }
|
||||||
|
|
||||||
|
@ -220,15 +215,18 @@ Page {
|
||||||
rightMargin: Theme.horizontalPageMargin
|
rightMargin: Theme.horizontalPageMargin
|
||||||
spacing: Theme.paddingLarge
|
spacing: Theme.paddingLarge
|
||||||
delegate: LibraryItemDelegate {
|
delegate: LibraryItemDelegate {
|
||||||
property string id: model.id
|
property string id: model.jellyfinId
|
||||||
title: model.name
|
title: model.name
|
||||||
poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags["primary"], "Primary", {"maxHeight": height})
|
poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.jellyfinId, model.imageTags["Primary"], "Primary", {"maxHeight": height})
|
||||||
blurhash: model.imageBlurHashes["primary"][model.imageTags["primary"]]
|
Binding on blurhash {
|
||||||
|
when: poster !== ""
|
||||||
|
value: model.imageBlurHashes["Primary"][model.imageTags["Primary"]]
|
||||||
|
}
|
||||||
landscape: !Utils.usePortraitCover(collectionType)
|
landscape: !Utils.usePortraitCover(collectionType)
|
||||||
progress: (typeof model.userData !== "undefined") ? model.userData.playedPercentage / 100 : 0.0
|
progress: (typeof model.userData !== "undefined") ? model.userData.playedPercentage / 100 : 0.0
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
pageStack.push(Utils.getPageUrl(model.mediaType, model.type, model.isFolder), {"itemId": model.id})
|
pageStack.push(Utils.getPageUrl(model.mediaType, model.type, model.isFolder), {"itemId": model.jellyfinId, "itemData": model.qtObject})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ Page {
|
||||||
VideoPlayer {
|
VideoPlayer {
|
||||||
id: videoPlayer
|
id: videoPlayer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
player: appWindow.mediaPlayer
|
manager: appWindow.playbackManager
|
||||||
title: itemData.name
|
title: itemData.name
|
||||||
audioTrack: videoPage.audioTrack
|
audioTrack: videoPage.audioTrack
|
||||||
subtitleTrack: videoPage.subtitleTrack
|
subtitleTrack: videoPage.subtitleTrack
|
||||||
|
@ -61,7 +61,7 @@ Page {
|
||||||
|
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
switch(status) {
|
switch(status) {
|
||||||
case PageStatus.Inactive:
|
case PageStatus.Deactivating:
|
||||||
videoPlayer.stop()
|
videoPlayer.stop()
|
||||||
break;
|
break;
|
||||||
case PageStatus.Active:
|
case PageStatus.Active:
|
||||||
|
|
|
@ -88,8 +88,7 @@ Page {
|
||||||
id: jItem
|
id: jItem
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
console.log("Status changed: " + newStatus, JSON.stringify(jItem))
|
//console.log("Status changed: " + newStatus, JSON.stringify(jItem))
|
||||||
console.log(jItem.mediaStreams)
|
|
||||||
if (status == JellyfinItem.Ready) {
|
if (status == JellyfinItem.Ready) {
|
||||||
updateBackdrop()
|
updateBackdrop()
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ BaseDetailPage {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
model: collectionModel
|
model: collectionModel
|
||||||
cellWidth: Constants.libraryDelegateWidth
|
cellWidth: Constants.libraryDelegateWidth
|
||||||
cellHeight: Utils.usePortraitCover(itemData.type) ? Constants.libraryDelegatePosterHeight
|
cellHeight: Utils.usePortraitCover(itemData.collectionType) ? Constants.libraryDelegatePosterHeight
|
||||||
: Constants.libraryDelegateHeight
|
: Constants.libraryDelegateHeight
|
||||||
visible: itemData.status !== JellyfinItem.Error
|
visible: itemData.status !== JellyfinItem.Error
|
||||||
|
|
||||||
|
@ -54,14 +54,14 @@ BaseDetailPage {
|
||||||
text: qsTr("Sort by")
|
text: qsTr("Sort by")
|
||||||
onClicked: pageStack.push(sortPageComponent)
|
onClicked: pageStack.push(sortPageComponent)
|
||||||
}
|
}
|
||||||
busy: collectionModel.status == ApiModel.Loading
|
busy: collectionModel.status === ApiModel.Loading
|
||||||
}
|
}
|
||||||
delegate: GridItem {
|
delegate: GridItem {
|
||||||
RemoteImage {
|
RemoteImage {
|
||||||
id: itemImage
|
id: itemImage
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags.primary, "Primary", {"maxWidth": width})
|
source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.jellyfinId, model.imageTags.Primary, "Primary", {"maxWidth": width})
|
||||||
blurhash: model.imageBlurHashes.primary[model.imageTags.primary]
|
blurhash: model.imageBlurHashes.Primary[model.imageTags.Primary]
|
||||||
fallbackColor: Utils.colorFromString(model.name)
|
fallbackColor: Utils.colorFromString(model.name)
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
clip: true
|
clip: true
|
||||||
|
@ -90,7 +90,7 @@ BaseDetailPage {
|
||||||
horizontalAlignment: Text.AlignLeft
|
horizontalAlignment: Text.AlignLeft
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
}
|
}
|
||||||
onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type, model.isFolder), {"itemId": model.id})
|
onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type, model.isFolder), {"itemId": model.jellyfinId})
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewPlaceholder {
|
ViewPlaceholder {
|
||||||
|
|
|
@ -29,7 +29,6 @@ import "../.."
|
||||||
BaseDetailPage {
|
BaseDetailPage {
|
||||||
id: albumPageRoot
|
id: albumPageRoot
|
||||||
readonly property int _songIndexWidth: 100
|
readonly property int _songIndexWidth: 100
|
||||||
property string _albumArtistText: itemData.albumArtist
|
|
||||||
width: 800 * Theme.pixelRatio
|
width: 800 * Theme.pixelRatio
|
||||||
|
|
||||||
readonly property bool _twoColumns: albumPageRoot.width / Theme.pixelRatio >= 800
|
readonly property bool _twoColumns: albumPageRoot.width / Theme.pixelRatio >= 800
|
||||||
|
@ -78,7 +77,7 @@ BaseDetailPage {
|
||||||
artists: model.artists
|
artists: model.artists
|
||||||
duration: model.runTimeTicks
|
duration: model.runTimeTicks
|
||||||
indexNumber: model.indexNumber
|
indexNumber: model.indexNumber
|
||||||
onClicked: window.playbackManager.playItem(model.id)
|
onClicked: window.playbackManager.playItem(model.jellyfinId)
|
||||||
}
|
}
|
||||||
|
|
||||||
VerticalScrollDecorator {}
|
VerticalScrollDecorator {}
|
||||||
|
@ -88,11 +87,6 @@ BaseDetailPage {
|
||||||
Connections {
|
Connections {
|
||||||
target: itemData
|
target: itemData
|
||||||
onAlbumArtistsChanged: {
|
onAlbumArtistsChanged: {
|
||||||
console.log(itemData.albumArtists)
|
|
||||||
_albumArtistText = ""
|
|
||||||
for (var i = 0; i < itemData.albumArtists.length; i++) {
|
|
||||||
_albumArtistText += itemData.albumArtists[i]["name"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +94,7 @@ BaseDetailPage {
|
||||||
item.albumArt = Qt.binding(function(){ return Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width})})
|
item.albumArt = Qt.binding(function(){ return Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width})})
|
||||||
item.name = Qt.binding(function(){ return itemData.name})
|
item.name = Qt.binding(function(){ return itemData.name})
|
||||||
item.releaseYear = Qt.binding(function() { return itemData.productionYear})
|
item.releaseYear = Qt.binding(function() { return itemData.productionYear})
|
||||||
item.albumArtist = Qt.binding(function() { return _albumArtistText})
|
item.albumArtist = Qt.binding(function() { return itemData.albumArtist})
|
||||||
item.duration = Qt.binding(function() { return itemData.runTimeTicks})
|
item.duration = Qt.binding(function() { return itemData.runTimeTicks})
|
||||||
item.songCount = Qt.binding(function() { return itemData.childCount})
|
item.songCount = Qt.binding(function() { return itemData.childCount})
|
||||||
item.listview = Qt.binding(function() { return list})
|
item.listview = Qt.binding(function() { return list})
|
||||||
|
|
|
@ -60,7 +60,8 @@ BaseDetailPage {
|
||||||
}
|
}
|
||||||
width: Constants.libraryDelegateWidth
|
width: Constants.libraryDelegateWidth
|
||||||
height: Constants.libraryDelegateHeight
|
height: Constants.libraryDelegateHeight
|
||||||
source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags.primary, "Primary", {"maxHeight": height})
|
source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.jellyfinId, model.imageTags.Primary, "Primary", {"maxHeight": height})
|
||||||
|
blurhash: model.imageBlurHashes.Primary[model.imageTags.Primary]
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
|
@ -140,7 +141,7 @@ BaseDetailPage {
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.id})
|
onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.jellyfinId})
|
||||||
}
|
}
|
||||||
|
|
||||||
VerticalScrollDecorator {}
|
VerticalScrollDecorator {}
|
||||||
|
|
|
@ -84,10 +84,10 @@ BaseDetailPage {
|
||||||
leftMargin: Theme.horizontalPageMargin
|
leftMargin: Theme.horizontalPageMargin
|
||||||
rightMargin: Theme.horizontalPageMargin
|
rightMargin: Theme.horizontalPageMargin
|
||||||
delegate: LibraryItemDelegate {
|
delegate: LibraryItemDelegate {
|
||||||
poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags.primary, "Primary", {"maxHeight": height})
|
poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.jellyfinId, model.imageTags.Primary, "Primary", {"maxHeight": height})
|
||||||
blurhash: model.imageBlurHashes["primary"][model.imageTags.primary]
|
blurhash: model.imageBlurHashes["Primary"][model.imageTags.Primary]
|
||||||
title: model.name
|
title: model.name
|
||||||
onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.id})
|
onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.jellyfinId})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ BaseDetailPage {
|
||||||
property alias subtitle: pageHeader.description
|
property alias subtitle: pageHeader.description
|
||||||
default property alias _data: content.data
|
default property alias _data: content.data
|
||||||
property real _playbackProsition: itemData.userData.playbackPositionTicks
|
property real _playbackProsition: itemData.userData.playbackPositionTicks
|
||||||
|
readonly property bool _userdataReady: itemData.status == JellyfinItem.Ready && itemData.userData != null
|
||||||
SilicaFlickable {
|
SilicaFlickable {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
contentHeight: content.height + Theme.paddingLarge
|
contentHeight: content.height + Theme.paddingLarge
|
||||||
|
@ -57,8 +58,14 @@ BaseDetailPage {
|
||||||
imageSource: Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width})
|
imageSource: Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width})
|
||||||
imageAspectRatio: Constants.horizontalVideoAspectRatio
|
imageAspectRatio: Constants.horizontalVideoAspectRatio
|
||||||
imageBlurhash: itemData.imageBlurHashes["Primary"][itemData.imageTags["Primary"]]
|
imageBlurhash: itemData.imageBlurHashes["Primary"][itemData.imageTags["Primary"]]
|
||||||
favourited: itemData.userData.isFavorite
|
Binding on favourited {
|
||||||
playProgress: itemData.userData.playedPercentage / 100
|
when: _userdataReady
|
||||||
|
value: itemData.userData.isFavorite
|
||||||
|
}
|
||||||
|
Binding on playProgress {
|
||||||
|
when: _userdataReady
|
||||||
|
value: itemData.userData.playedPercentage / 100
|
||||||
|
}
|
||||||
onPlayPressed: pageStack.push(Qt.resolvedUrl("../VideoPage.qml"),
|
onPlayPressed: pageStack.push(Qt.resolvedUrl("../VideoPage.qml"),
|
||||||
{"itemData": itemData,
|
{"itemData": itemData,
|
||||||
"audioTrack": trackSelector.audioTrack,
|
"audioTrack": trackSelector.audioTrack,
|
||||||
|
|
|
@ -31,6 +31,7 @@ Dialog {
|
||||||
id: loginDialog
|
id: loginDialog
|
||||||
property string loginMessage
|
property string loginMessage
|
||||||
property Page firstPage
|
property Page firstPage
|
||||||
|
property User selectedUser: null
|
||||||
|
|
||||||
property string error
|
property string error
|
||||||
|
|
||||||
|
@ -92,20 +93,29 @@ Dialog {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
||||||
Flow {
|
Flow {
|
||||||
|
id: userList
|
||||||
width: parent.width
|
width: parent.width
|
||||||
Repeater {
|
Repeater {
|
||||||
|
id: userRepeater
|
||||||
model: userModel
|
model: userModel
|
||||||
delegate: UserGridDelegate {
|
delegate: UserGridDelegate {
|
||||||
name: model.name
|
name: model.name
|
||||||
image: model.primaryImageTag ? "%1/Users/%2/Images/Primary?tag=%3".arg(ApiClient.baseUrl).arg(model.id).arg(model.primaryImageTag) : ""
|
image: model.primaryImageTag ? "%1/Users/%2/Images/Primary?tag=%3".arg(ApiClient.baseUrl).arg(model.jellyfinId).arg(model.primaryImageTag) : ""
|
||||||
highlighted: model.name === username.text
|
highlighted: model.name === username.text
|
||||||
|
onHighlightedChanged: {
|
||||||
|
if (highlighted) {
|
||||||
|
selectedUser = model.qtObject
|
||||||
|
}
|
||||||
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
username.text = model.name
|
username.text = model.name
|
||||||
|
if (!password.activeFocus) {
|
||||||
password.focus = true
|
password.focus = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SectionHeader {
|
SectionHeader {
|
||||||
//: Section header for entering username and password
|
//: Section header for entering username and password
|
||||||
|
@ -119,18 +129,23 @@ Dialog {
|
||||||
placeholderText: qsTr("Username")
|
placeholderText: qsTr("Username")
|
||||||
label: placeholderText
|
label: placeholderText
|
||||||
errorHighlight: error
|
errorHighlight: error
|
||||||
EnterKey.iconSource: "image://theme/icon-m-enter-next"
|
EnterKey.iconSource: "image://theme/icon-m-enter-" + (password.enabled ? "next" : "accept")
|
||||||
EnterKey.onClicked: password.focus = true
|
EnterKey.onClicked: password.enabled ? password.focus = true : accept()
|
||||||
|
onTextChanged: {
|
||||||
|
// Wil be executed before the onHighlightChanged of the UserDelegate
|
||||||
|
// This is done to update the UI after an user is selected and this field has
|
||||||
|
// been changed, to not let the UI be in an invalid state (e.g. no user selected, password field disabled)
|
||||||
|
selectedUser = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField {
|
PasswordField {
|
||||||
id: password
|
id: password
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
||||||
//: Label placeholder for password field
|
//: Label placeholder for password field
|
||||||
placeholderText: qsTr("Password")
|
placeholderText: qsTr("Password")
|
||||||
label: placeholderText
|
label: placeholderText
|
||||||
echoMode: TextInput.Password
|
|
||||||
errorHighlight: error
|
errorHighlight: error
|
||||||
|
|
||||||
EnterKey.iconSource: "image://theme/icon-m-enter-accept"
|
EnterKey.iconSource: "image://theme/icon-m-enter-accept"
|
||||||
|
@ -169,4 +184,41 @@ Dialog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
canAccept: username.text
|
canAccept: username.text
|
||||||
|
|
||||||
|
states: [
|
||||||
|
State {
|
||||||
|
name: "noUsers"
|
||||||
|
when: userRepeater.count == 0
|
||||||
|
PropertyChanges {
|
||||||
|
target: userList
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "users"
|
||||||
|
when: userRepeater.count != 0 && selectedUser === null
|
||||||
|
PropertyChanges {
|
||||||
|
target: userList
|
||||||
|
visible: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "selectedUserPassword"
|
||||||
|
when: selectedUser !== null && selectedUser.hasPassword
|
||||||
|
extend: "users"
|
||||||
|
PropertyChanges {
|
||||||
|
target: password
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "selectedUserNoPassword"
|
||||||
|
when: selectedUser !== null && !selectedUser.hasPassword
|
||||||
|
extend: "users"
|
||||||
|
PropertyChanges {
|
||||||
|
target: password
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
#include <QtQuick>
|
#include <QtQuick>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <QQmlDebuggingEnabler>
|
||||||
#include <QCommandLineOption>
|
#include <QCommandLineOption>
|
||||||
#include <QCommandLineParser>
|
#include <QCommandLineParser>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
@ -37,6 +38,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
static const char *SANDBOX_PROGRAM = "/usr/bin/sailjail";
|
static const char *SANDBOX_PROGRAM = "/usr/bin/sailjail";
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
|
QQmlDebuggingEnabler enabler;
|
||||||
|
enabler.startTcpDebugServer(9999);
|
||||||
// SailfishApp::main() will display "qml/harbour-sailfin.qml", if you need more
|
// SailfishApp::main() will display "qml/harbour-sailfin.qml", if you need more
|
||||||
// control over initialization, you can use:
|
// control over initialization, you can use:
|
||||||
//
|
//
|
||||||
|
|
Loading…
Reference in a new issue