1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-05-11 00:22:43 +00:00

Rewire more of Sailfish frontend into new backend

This should encompass most simple things, besides some larger, trickier
things, like the video streams and the now-broken userdata
This commit is contained in:
Chris Josten 2021-08-11 23:35:33 +02:00
parent df1e134821
commit 7b6c272aa9
47 changed files with 620 additions and 291 deletions

View file

@ -19,6 +19,7 @@ set(JellyfinQt_SOURCES
src/viewmodel/modelstatus.cpp src/viewmodel/modelstatus.cpp
src/viewmodel/playbackmanager.cpp src/viewmodel/playbackmanager.cpp
src/viewmodel/playlist.cpp src/viewmodel/playlist.cpp
src/viewmodel/userdata.cpp
src/apiclient.cpp src/apiclient.cpp
src/apimodel.cpp src/apimodel.cpp
src/credentialmanager.cpp src/credentialmanager.cpp
@ -46,6 +47,7 @@ set(JellyfinQt_HEADERS
include/JellyfinQt/viewmodel/propertyhelper.h include/JellyfinQt/viewmodel/propertyhelper.h
include/JellyfinQt/viewmodel/playbackmanager.h include/JellyfinQt/viewmodel/playbackmanager.h
include/JellyfinQt/viewmodel/playlist.h include/JellyfinQt/viewmodel/playlist.h
include/JellyfinQt/viewmodel/userdata.h
include/JellyfinQt/apiclient.h include/JellyfinQt/apiclient.h
include/JellyfinQt/apimodel.h include/JellyfinQt/apimodel.h
include/JellyfinQt/credentialmanager.h include/JellyfinQt/credentialmanager.h

View file

@ -124,7 +124,7 @@ public:
Q_ENUM(ApiError) Q_ENUM(ApiError)
const QString &baseUrl() const { return this->m_baseUrl; } const QString &baseUrl() const { return this->m_baseUrl; }
const QString &userId() const { return m_userId; } const QString userId() const { return m_userId; }
const QString &deviceId() const { return m_deviceId; } const QString &deviceId() const { return m_deviceId; }
/** /**
* @brief QML applications can set this type to indicate which commands they support. * @brief QML applications can set this type to indicate which commands they support.
@ -275,7 +275,7 @@ private:
QString m_token; QString m_token;
QString m_deviceName; QString m_deviceName;
QString m_deviceId; QString m_deviceId;
QString m_userId = ""; QString m_userId;
QJsonObject m_deviceProfile; QJsonObject m_deviceProfile;
QJsonObject m_playbackDeviceProfile; QJsonObject m_playbackDeviceProfile;
bool m_online = true; bool m_online = true;

View file

@ -188,7 +188,7 @@ public:
* @return pair containing the items loaded and the integer containing the starting offset. A starting * @return pair containing the items loaded and the integer containing the starting offset. A starting
* offset of -1 means an error has occurred. * offset of -1 means an error has occurred.
*/ */
std::pair<QList<T>, int> &&result() { return std::move(m_result); } std::pair<QList<T*>, int> &&result() { return std::move(m_result); }
protected: protected:
/** /**
* @brief Loads data from the given offset with a maximum count of limit. * @brief Loads data from the given offset with a maximum count of limit.
@ -205,7 +205,7 @@ protected:
m_startIndex = startIndex; m_startIndex = startIndex;
m_totalRecordCount = totalRecordCount; m_totalRecordCount = totalRecordCount;
} }
std::pair<QList<T>, int> m_result; std::pair<QList<T*>, int> m_result;
}; };
/** /**
@ -275,7 +275,7 @@ protected:
// If futureWatcher's future is running, this method should not be called again. // If futureWatcher's future is running, this method should not be called again.
if (m_futureWatcher.isRunning()) return; if (m_futureWatcher.isRunning()) return;
// Set an invalid result. // Set an invalid result.
this->m_result = { QList<T>(), -1 }; this->m_result = { QList<T*>(), -1 };
if (!setRequestStartIndex<P>(this->m_parameters, offset) if (!setRequestStartIndex<P>(this->m_parameters, offset)
&& suggestedModelStatus == ViewModel::ModelStatus::LoadingMore) { && suggestedModelStatus == ViewModel::ModelStatus::LoadingMore) {
@ -326,12 +326,12 @@ protected:
if (totalRecordCount < 0) { if (totalRecordCount < 0) {
totalRecordCount = records.size(); totalRecordCount = records.size();
} }
QList<T> models; QList<T*> models;
models.reserve(records.size()); models.reserve(records.size());
// Convert the DTOs into models // Convert the DTOs into models
for (int i = 0; i < records.size(); i++) { for (int i = 0; i < records.size(); i++) {
models.append(T(records[i], m_loader->apiClient())); models.append(new T(records[i], m_loader->apiClient()));
} }
this->setStatus(ViewModel::ModelStatus::Ready); this->setStatus(ViewModel::ModelStatus::Ready);
this->m_result = { models, this->m_startIndex}; this->m_result = { models, this->m_startIndex};
@ -441,7 +441,7 @@ public:
} }
// QList-like API // QList-like API
const T& at(int index) const { return m_array.at(index); } QSharedPointer<T> at(int index) const { return m_array.at(index); }
/** /**
* @return the amount of objects in this model. * @return the amount of objects in this model.
*/ */
@ -449,22 +449,23 @@ public:
return m_array.size(); return m_array.size();
} }
void insert(int index, T &object) { void insert(int index, QSharedPointer<T> object) {
Q_ASSERT(index >= 0 && index <= size()); Q_ASSERT(index >= 0 && index <= size());
this->beginInsertRows(QModelIndex(), index, index); this->beginInsertRows(QModelIndex(), index, index);
m_array.insert(index, object); m_array.insert(index, object);
this->endInsertRows(); this->endInsertRows();
} }
void append(T &object) { insert(size(), object); } void append(QSharedPointer<T> object) { insert(size(), object); }
void append(QList<T> &objects) { //void append(T &object) { insert(size(), object); }
void append(QList<QSharedPointer<T>> &objects) {
int index = size(); int index = size();
this->beginInsertRows(QModelIndex(), index, index + objects.size() - 1); this->beginInsertRows(QModelIndex(), index, index + objects.size() - 1);
m_array.append(objects); m_array.append(objects);
this->endInsertRows(); this->endInsertRows();
}; };
QList<T> mid(int pos, int length = -1) { QList<T*> mid(int pos, int length = -1) {
return m_array.mid(pos, length); return m_array.mid(pos, length);
} }
@ -483,7 +484,7 @@ public:
this->endRemoveRows(); this->endRemoveRows();
} }
void removeOne(T &object) { void removeOne(QSharedPointer<T> object) {
int idx = m_array.indexOf(object); int idx = m_array.indexOf(object);
if (idx >= 0) { if (idx >= 0) {
removeAt(idx); removeAt(idx);
@ -535,20 +536,26 @@ public:
protected: protected:
// Model-specific properties. // Model-specific properties.
QList<T> m_array; QList<QSharedPointer<T>> m_array;
ModelLoader<T> *m_loader = nullptr; ModelLoader<T> *m_loader = nullptr;
void loadingFinished() override { void loadingFinished() override {
Q_ASSERT(m_loader != nullptr); Q_ASSERT(m_loader != nullptr);
std::pair<QList<T>, int> result = m_loader->result(); std::pair<QList<T*>, int> result = m_loader->result();
qDebug() << "Results loaded: index: " << result.second << ", count: " << result.first.size(); qDebug() << "Results loaded: index: " << result.second << ", count: " << result.first.size();
if (result.second == -1) { if (result.second == -1) {
clear(); clear();
} else if (result.second == m_array.size()) { } else if (result.second == m_array.size()) {
append(result.first); m_array.reserve(m_array.size() + result.second);
for (auto it = result.first.begin(); it != result.first.end(); it++) {
append(QSharedPointer<T>(*it));
}
} else if (result.second < m_array.size()){ } else if (result.second < m_array.size()){
removeUntilEnd(result.second); removeUntilEnd(result.second);
append(result.first); m_array.reserve(m_array.size() + result.second);
for (auto it = result.first.begin(); it != result.first.end(); it++) {
append(QSharedPointer<T>(*it));
}
} else { } else {
// result.second > m_array.size() // result.second > m_array.size()
qWarning() << "Retrieved data from loader at position bigger than size()"; qWarning() << "Retrieved data from loader at position bigger than size()";

View file

@ -123,7 +123,6 @@ public:
private: private:
QString urlToGroupName(const QString &url) const; QString urlToGroupName(const QString &url) const;
QString groupNameToUrl(const QString &group) const;
QSettings m_settings; QSettings m_settings;
}; };

View file

@ -20,9 +20,11 @@
#ifndef JELLYFIN_MODEL_ITEM #ifndef JELLYFIN_MODEL_ITEM
#define JELLYFIN_MODEL_ITEM #define JELLYFIN_MODEL_ITEM
#include <functional>
#include <map>
#include <QObject>
#include <QList> #include <QList>
#include <QThreadPool>
#include <QtConcurrent/QtConcurrent>
#include "../dto/baseitemdto.h" #include "../dto/baseitemdto.h"
#include "../support/loader.h" #include "../support/loader.h"
@ -31,20 +33,22 @@
namespace Jellyfin { namespace Jellyfin {
namespace Model { namespace Model {
class Item : public DTO::BaseItemDto { class Item : public QObject, public DTO::BaseItemDto {
Q_OBJECT
public: public:
using UserDataChangedCallback = std::function<void(DTO::UserItemDataDto)>;
/** /**
* @brief Constructor that creates an empty item. * @brief Constructor that creates an empty item.
*/ */
Item(); Item(QObject *parent = nullptr);
/** /**
* @brief Copies the data from the DTO into this model and attaches an ApiClient * @brief Copies the data from the DTO into this model and attaches an ApiClient
* @param data The DTO to copy information from * @param data The DTO to copy information from
* @param apiClient The ApiClient to attach to, to listen for updates and so on. * @param apiClient The ApiClient to attach to, to listen for updates and so on.
*/ */
Item(const DTO::BaseItemDto &data, ApiClient *apiClient = nullptr); Item(const DTO::BaseItemDto &data, ApiClient *apiClient = nullptr, QObject *parent = nullptr);
virtual ~Item(); //virtual ~Item();
/** /**
* @brief sameAs Returns true if this item represents the same item as `other` * @brief sameAs Returns true if this item represents the same item as `other`
@ -58,11 +62,13 @@ public:
bool sameAs(const DTO::BaseItemDto &other); bool sameAs(const DTO::BaseItemDto &other);
void setApiClient(ApiClient *apiClient); void setApiClient(ApiClient *apiClient);
signals:
void userDataChanged(const DTO::UserItemDataDto &newUserData);
private: private:
ApiClient *m_apiClient = nullptr; ApiClient *m_apiClient = nullptr;
QList<QMetaObject::Connection> m_apiClientConnections; void updateUserData(const QString &itemId, const DTO::UserItemDataDto &userData);
void onUserDataUpdated(const QString &itemId, const DTO::UserItemDataDto &userData);
}; };
} }

View file

@ -76,7 +76,7 @@ public:
/** /**
* @brief Appends all items from the given itemModel to this list * @brief Appends all items from the given itemModel to this list
*/ */
void appendToList(const ViewModel::ItemModel &model); void appendToList(ViewModel::ItemModel &model);
/** /**
* @brief Start playing this playlist * @brief Start playing this playlist

View file

@ -46,15 +46,22 @@
#include "loader.h" #include "loader.h"
namespace Jellyfin { namespace Jellyfin {
namespace DTO {
class UserItemDataDto;
} // NS DTO
namespace ViewModel { namespace ViewModel {
class UserData;
class Item : public QObject { class Item : public QObject {
Q_OBJECT Q_OBJECT
public: public:
explicit Item(QObject *parent = nullptr, QSharedPointer<Model::Item> data = QSharedPointer<Model::Item>::create()); explicit Item(QObject *parent = nullptr, QSharedPointer<Model::Item> data = QSharedPointer<Model::Item>::create());
// Please keep the order of the properties the same as in the file linked above. // Please keep the order of the properties the same as in the file linked above.
Q_PROPERTY(QUuid jellyfinId READ jellyfinId NOTIFY jellyfinIdChanged) Q_PROPERTY(QString jellyfinId READ jellyfinId NOTIFY jellyfinIdChanged)
Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(QString originalTitle READ originalTitle NOTIFY originalTitleChanged) Q_PROPERTY(QString originalTitle READ originalTitle NOTIFY originalTitleChanged)
Q_PROPERTY(QString serverId READ serverId NOTIFY serverIdChanged) Q_PROPERTY(QString serverId READ serverId NOTIFY serverIdChanged)
@ -81,27 +88,29 @@ public:
//SKIP: ExternalUrls //SKIP: ExternalUrls
//SKIP: MediaSources //SKIP: MediaSources
Q_PROPERTY(float criticRating READ criticRating NOTIFY criticRatingChanged) Q_PROPERTY(float criticRating READ criticRating NOTIFY criticRatingChanged)
Q_PROPERTY(QStringList productionLocations READ productionLocations NOTIFY productionLocationsChanged) Q_PROPERTY(QStringList productionLocations READ productionLocations NOTIFY productionLocationsChanged)*/
// Handpicked, important ones // Handpicked, important ones
Q_PROPERTY(qint64 runTimeTicks READ runTimeTicks NOTIFY runTimeTicksChanged)*/ Q_PROPERTY(qint64 runTimeTicks READ runTimeTicks NOTIFY runTimeTicksChanged)
Q_PROPERTY(QString overview READ overview NOTIFY overviewChanged) Q_PROPERTY(QString overview READ overview NOTIFY overviewChanged)
Q_PROPERTY(int productionYear READ productionYear NOTIFY productionYearChanged) Q_PROPERTY(int productionYear READ productionYear NOTIFY productionYearChanged)
Q_PROPERTY(int indexNumber READ indexNumber NOTIFY indexNumberChanged) Q_PROPERTY(int indexNumber READ indexNumber NOTIFY indexNumberChanged)
Q_PROPERTY(int indexNumberEnd READ indexNumberEnd NOTIFY indexNumberEndChanged) Q_PROPERTY(int indexNumberEnd READ indexNumberEnd NOTIFY indexNumberEndChanged)
Q_PROPERTY(bool isFolder READ isFolder NOTIFY isFolderChanged) Q_PROPERTY(bool isFolder READ isFolder NOTIFY isFolderChanged)
Q_PROPERTY(QString type READ type NOTIFY typeChanged) Q_PROPERTY(QString type READ type NOTIFY typeChanged)
/*Q_PROPERTY(QString parentBackdropItemId READ parentBackdropItemId NOTIFY parentBackdropItemIdChanged) Q_PROPERTY(QString parentBackdropItemId READ parentBackdropItemId NOTIFY parentBackdropItemIdChanged)
Q_PROPERTY(QStringList parentBackdropImageTags READ parentBackdropImageTags NOTIFY parentBackdropImageTagsChanged) Q_PROPERTY(QStringList parentBackdropImageTags READ parentBackdropImageTags NOTIFY parentBackdropImageTagsChanged)
Q_PROPERTY(UserData *userData READ userData NOTIFY userDataChanged) Q_PROPERTY(UserData *userData READ userData NOTIFY userDataChanged)
Q_PROPERTY(int recursiveItemCount READ recursiveItemCount NOTIFY recursiveItemCountChanged) Q_PROPERTY(int recursiveItemCount READ recursiveItemCount NOTIFY recursiveItemCountChanged)
Q_PROPERTY(int childCount READ childCount NOTIFY childCountChanged) Q_PROPERTY(int childCount READ childCount NOTIFY childCountChanged)
Q_PROPERTY(QString albumArtist READ albumArtist NOTIFY albumArtistChanged) Q_PROPERTY(QString albumArtist READ albumArtist NOTIFY albumArtistChanged)
Q_PROPERTY(QVariantList albumArtists READ albumArtists NOTIFY albumArtistsChanged) /*Q_PROPERTY(QVariantList albumArtists READ albumArtists NOTIFY albumArtistsChanged)*/
Q_PROPERTY(QString seriesName READ seriesName NOTIFY seriesNameChanged) Q_PROPERTY(QString seriesName READ seriesName NOTIFY seriesNameChanged)
Q_PROPERTY(QString seriesId READ seriesId NOTIFY seriesIdChanged)
Q_PROPERTY(QString seasonId READ seasonId NOTIFY seasonIdChanged)
Q_PROPERTY(QString seasonName READ seasonName NOTIFY seasonNameChanged) Q_PROPERTY(QString seasonName READ seasonName NOTIFY seasonNameChanged)
Q_PROPERTY(QList<MediaStream *> __list__mediaStreams MEMBER __list__m_mediaStreams NOTIFY mediaStreamsChanged) /*Q_PROPERTY(QList<MediaStream *> __list__mediaStreams MEMBER __list__m_mediaStreams NOTIFY mediaStreamsChanged)
Q_PROPERTY(QVariantList mediaStreams READ mediaStreams NOTIFY mediaStreamsChanged STORED false) Q_PROPERTY(QVariantList mediaStreams READ mediaStreams NOTIFY mediaStreamsChanged STORED false)*/
Q_PROPERTY(QStringList artists READ artists NOTIFY artistsChanged) Q_PROPERTY(QStringList artists READ artists NOTIFY artistsChanged)
// Why is this a QJsonObject? Well, because I couldn't be bothered to implement the deserialisations of // Why is this a QJsonObject? Well, because I couldn't be bothered to implement the deserialisations of
// a QHash at the moment. // a QHash at the moment.
@ -110,9 +119,9 @@ public:
Q_PROPERTY(QJsonObject imageBlurHashes READ imageBlurHashes NOTIFY imageBlurHashesChanged) Q_PROPERTY(QJsonObject imageBlurHashes READ imageBlurHashes NOTIFY imageBlurHashesChanged)
Q_PROPERTY(QString mediaType READ mediaType READ mediaType NOTIFY mediaTypeChanged) Q_PROPERTY(QString mediaType READ mediaType READ mediaType NOTIFY mediaTypeChanged)
Q_PROPERTY(int width READ width NOTIFY widthChanged) Q_PROPERTY(int width READ width NOTIFY widthChanged)
Q_PROPERTY(int height READ height NOTIFY heightChanged)*/ Q_PROPERTY(int height READ height NOTIFY heightChanged)
QUuid jellyfinId() const { return m_data->jellyfinId(); } QString jellyfinId() const { return m_data->jellyfinId(); }
QString name() const { return m_data->name(); } QString name() const { return m_data->name(); }
QString originalTitle() const { return m_data->originalTitle(); } QString originalTitle() const { return m_data->originalTitle(); }
QString serverId() const { return m_data->serverId(); } QString serverId() const { return m_data->serverId(); }
@ -125,17 +134,36 @@ public:
int airsBeforeSeasonNumber() const { return m_data->airsBeforeSeasonNumber().value_or(0); } int airsBeforeSeasonNumber() const { return m_data->airsBeforeSeasonNumber().value_or(0); }
int airsAfterSeasonNumber() const { return m_data->airsAfterSeasonNumber().value_or(999); } int airsAfterSeasonNumber() const { return m_data->airsAfterSeasonNumber().value_or(999); }
int airsBeforeEpisodeNumber() const { return m_data->airsBeforeEpisodeNumber().value_or(0); } int airsBeforeEpisodeNumber() const { return m_data->airsBeforeEpisodeNumber().value_or(0); }
qint64 runTimeTicks() const { return m_data->runTimeTicks().value_or(0); }
QString overview() const { return m_data->overview(); } QString overview() const { return m_data->overview(); }
int productionYear() const { return m_data->productionYear().value_or(0); } int productionYear() const { return m_data->productionYear().value_or(0); }
int indexNumber() const { return m_data->indexNumber().value_or(-1); } int indexNumber() const { return m_data->indexNumber().value_or(-1); }
int indexNumberEnd() const { return m_data->indexNumberEnd().value_or(-1); } int indexNumberEnd() const { return m_data->indexNumberEnd().value_or(-1); }
bool isFolder() const { return m_data->isFolder().value_or(false); } bool isFolder() const { return m_data->isFolder().value_or(false); }
QString type() const { return m_data->type(); } QString type() const { return m_data->type(); }
QString parentBackdropItemId() const { return m_data->parentBackdropItemId(); }
QStringList parentBackdropImageTags() const { return m_data->parentBackdropImageTags(); }
UserData *userData() const { return m_userData; }
int recursiveItemCount() const { return m_data->recursiveItemCount().value_or(0); }
int childCount() const { return m_data->childCount().value_or(0); }
QString albumArtist() const { return m_data->albumArtist(); }
QString seriesName() const { return m_data->seriesName(); }
QString seriesId() const { return m_data->seriesId(); }
QString seasonId() const { return m_data->seasonId(); }
QString seasonName() const { return m_data->seasonName(); }
QStringList artists() const { return m_data->artists(); }
QJsonObject imageTags() const { return m_data->imageTags(); }
QStringList backdropImageTags() const { return m_data->backdropImageTags(); }
QJsonObject imageBlurHashes() const { return m_data->imageBlurHashes(); }
QString mediaType() const { return m_data->mediaType(); }
int width() const { return m_data->width().value_or(0); }
int height() const { return m_data->height().value_or(0); }
QSharedPointer<Model::Item> data() const { return m_data; } QSharedPointer<Model::Item> data() const { return m_data; }
void setData(QSharedPointer<Model::Item> newData); void setData(QSharedPointer<Model::Item> newData);
signals: signals:
void jellyfinIdChanged(const QUuid &newId); void jellyfinIdChanged(const QString &newId);
void nameChanged(const QString &newName); void nameChanged(const QString &newName);
void originalTitleChanged(const QString &newOriginalTitle); void originalTitleChanged(const QString &newOriginalTitle);
void serverIdChanged(const QString &newServerId); void serverIdChanged(const QString &newServerId);
@ -171,12 +199,14 @@ signals:
void typeChanged(const QString &newType); void typeChanged(const QString &newType);
void parentBackdropItemIdChanged(); void parentBackdropItemIdChanged();
void parentBackdropImageTagsChanged(); void parentBackdropImageTagsChanged();
//void userDataChanged(UserData *newUserData); void userDataChanged(UserData *newUserData);
void recursiveItemCountChanged(int newRecursiveItemCount); void recursiveItemCountChanged(int newRecursiveItemCount);
void childCountChanged(int newChildCount); void childCountChanged(int newChildCount);
void albumArtistChanged(const QString &newAlbumArtist); void albumArtistChanged(const QString &newAlbumArtist);
//void albumArtistsChanged(NameGuidPair *newAlbumArtists); //void albumArtistsChanged(NameGuidPair *newAlbumArtists);
void seriesNameChanged(const QString &newSeriesName); void seriesNameChanged(const QString &newSeriesName);
void seriesIdChanged(const QString &newSeriesId);
void seasonIdChanged(const QString &newSeasonId);
void seasonNameChanged(const QString &newSeasonName); void seasonNameChanged(const QString &newSeasonName);
void mediaStreamsChanged(/*const QList<MediaStream *> &newMediaStreams*/); void mediaStreamsChanged(/*const QList<MediaStream *> &newMediaStreams*/);
void artistsChanged(const QStringList &newArtists); void artistsChanged(const QStringList &newArtists);
@ -187,7 +217,13 @@ signals:
void widthChanged(int newWidth); void widthChanged(int newWidth);
void heightChanged(int newHeight); void heightChanged(int newHeight);
protected: protected:
void setUserData(DTO::UserItemDataDto &newData);
void setUserData(QSharedPointer<DTO::UserItemDataDto> newData);
QSharedPointer<Model::Item> m_data; QSharedPointer<Model::Item> m_data;
UserData *m_userData = nullptr;
private slots:
void onUserDataChanged(const DTO::UserItemDataDto &userData);
}; };
class ItemLoader : public Loader<ViewModel::Item, DTO::BaseItemDto, Jellyfin::Loader::GetItemParams> { class ItemLoader : public Loader<ViewModel::Item, DTO::BaseItemDto, Jellyfin::Loader::GetItemParams> {

View file

@ -134,12 +134,108 @@ public:
FWDPROP(QList<Jellyfin::DTO::LocationTypeClass::Value>, excludeLocationTypes, ExcludeLocationTypes) FWDPROP(QList<Jellyfin::DTO::LocationTypeClass::Value>, excludeLocationTypes, ExcludeLocationTypes)
FWDPROP(QList<Jellyfin::DTO::ItemFieldsClass::Value>, fields, Fields) FWDPROP(QList<Jellyfin::DTO::ItemFieldsClass::Value>, fields, Fields)
FWDPROP(QList<Jellyfin::DTO::ItemFilterClass::Value>, filters, Filters) FWDPROP(QList<Jellyfin::DTO::ItemFilterClass::Value>, filters, Filters)
FWDPROP(QStringList, genreIds, GenreIds)
FWDPROP(QStringList, genres, Genres)
FWDPROP(bool, hasImdbId, HasImdbId)
FWDPROP(bool, hasOfficialRating, HasOfficialRating)
FWDPROP(bool, hasOverview, HasOverview)
FWDPROP(bool, hasParentalRating, HasParentalRating)
FWDPROP(bool, hasSpecialFeature, HasSpecialFeature)
FWDPROP(bool, hasSubtitles, HasSubtitles)
FWDPROP(bool, hasThemeSong, HasThemeSong)
FWDPROP(bool, hasThemeVideo, HasThemeVideo)
FWDPROP(bool, hasTmdbId, HasTmdbId)
FWDPROP(bool, hasTrailer, HasTrailer)
FWDPROP(bool, hasTvdbId, HasTvdbId)
FWDPROP(QStringList, ids, Ids)
FWDPROP(qint32, imageTypeLimit, ImageTypeLimit)
FWDPROP(QList<Jellyfin::DTO::ImageTypeClass::Value>, imageTypes, ImageTypes)
FWDPROP(QStringList, includeItemTypes, IncludeItemTypes)
FWDPROP(bool, is3D, Is3D)
FWDPROP(bool, is4K, Is4K)
FWDPROP(bool, isFavorite, IsFavorite)
FWDPROP(bool, isHd, IsHd)
FWDPROP(bool, isLocked, IsLocked)
FWDPROP(bool, isMissing, IsMissing)
FWDPROP(bool, isPlaceHolder, IsPlaceHolder)
FWDPROP(bool, isPlayed, IsPlayed)
FWDPROP(bool, isUnaired, IsUnaired)
FWDPROP(QList<Jellyfin::DTO::LocationTypeClass::Value>, locationTypes, LocationTypes)
FWDPROP(qint32, maxHeight, MaxHeight)
FWDPROP(QString, maxOfficialRating, MaxOfficialRating)
FWDPROP(QDateTime, maxPremiereDate, MaxPremiereDate)
FWDPROP(qint32, maxWidth, MaxWidth)
FWDPROP(QStringList, mediaTypes, MediaTypes)
FWDPROP(qint32, minHeight, MinHeight)
FWDPROP(QString, minOfficialRating, MinOfficialRating)
FWDPROP(QDateTime, minPremiereDate, MinPremiereDate)
FWDPROP(qint32, minWidth, MinWidth)
FWDPROP(QString, sortBy, SortBy)
FWDPROP(QString, sortOrder, SortOrder)
FWDPROP(QStringList, tags, Tags)
FWDPROP(QList<qint32>, years, Years)
FWDPROP(QString, parentId, ParentId) FWDPROP(QString, parentId, ParentId)
FWDPROP(bool, recursive, Recursive) FWDPROP(bool, recursive, Recursive)
FWDPROP(QString, searchTerm, SearchTerm)
//FWDPROP(bool, collapseBoxSetItems) //FWDPROP(bool, collapseBoxSetItems)
}; };
using ResumeItemsLoaderBase = AbstractUserParameterLoader<Model::Item, DTO::BaseItemDto, DTO::BaseItemDtoQueryResult, Jellyfin::Loader::GetResumeItemsParams>;
class ResumeItemsLoader : public ResumeItemsLoaderBase {
Q_OBJECT
public:
explicit ResumeItemsLoader(QObject *parent = nullptr);
FWDPROP(QList<Jellyfin::DTO::ImageTypeClass::Value>, enableImageTypes, EnableImageTypes);
FWDPROP(bool, enableImages, EnableImages)
FWDPROP(bool, enableTotalRecordCount, EnableTotalRecordCount)
FWDPROP(bool, enableUserData, EnableUserData)
FWDPROP(QStringList, excludeItemTypes, ExcludeItemTypes)
FWDPROP(QList<Jellyfin::DTO::ItemFieldsClass::Value>, fields, Fields)
FWDPROP(qint32, imageTypeLimit, ImageTypeLimit)
FWDPROP(QStringList, includeItemTypes, IncludeItemTypes)
FWDPROP(QStringList, mediaTypes, MediaTypes)
FWDPROP(QString, parentId, ParentId)
FWDPROP(QString, searchTerm, SearchTerm)
};
using ShowSeasonsLoaderBase = AbstractUserParameterLoader<Model::Item, DTO::BaseItemDto, DTO::BaseItemDtoQueryResult, Jellyfin::Loader::GetSeasonsParams>;
class ShowSeasonsLoader : public ShowSeasonsLoaderBase {
Q_OBJECT
public:
explicit ShowSeasonsLoader(QObject *parent = nullptr);
FWDPROP(QString, seriesId, SeriesId)
FWDPROP(QString, adjacentTo, AdjacentTo)
FWDPROP(QList<Jellyfin::DTO::ImageTypeClass::Value>, enableImageTypes, EnableImageTypes)
FWDPROP(bool, enableImages, EnableImages)
FWDPROP(bool, enableUserData, EnableUserData)
FWDPROP(QList<Jellyfin::DTO::ItemFieldsClass::Value>, fields, Fields)
FWDPROP(qint32, imageTypeLimit, ImageTypeLimit)
FWDPROP(bool, isMissing, IsMissing)
FWDPROP(bool, isSpecialSeason, IsSpecialSeason)
};
using ShowEpisodesLoaderBase = AbstractUserParameterLoader<Model::Item, DTO::BaseItemDto, DTO::BaseItemDtoQueryResult, Jellyfin::Loader::GetEpisodesParams>;
class ShowEpisodesLoader : public ShowEpisodesLoaderBase {
Q_OBJECT
public:
explicit ShowEpisodesLoader(QObject *parent = nullptr);
FWDPROP(QString, seriesId, SeriesId)
FWDPROP(QString, adjacentTo, AdjacentTo)
FWDPROP(bool, enableImages, EnableImages)
FWDPROP(bool, enableUserData, EnableUserData)
FWDPROP(QList<Jellyfin::DTO::ItemFieldsClass::Value>, fields, Fields)
FWDPROP(qint32, imageTypeLimit, ImageTypeLimit)
FWDPROP(bool, isMissing, IsMissing)
FWDPROP(qint32, season, Season)
FWDPROP(QString, seasonId, SeasonId)
FWDPROP(QString, sortBy, SortBy)
FWDPROP(QString, startItemId, StartItemId)
};
/** /**
@ -167,6 +263,11 @@ public:
mediaType, mediaType,
type, type,
collectionType, collectionType,
indexNumber,
runTimeTicks,
artists,
isFolder,
parentIndexNumber,
jellyfinExtendModelAfterHere = Qt::UserRole + 300 // Should be enough for now jellyfinExtendModelAfterHere = Qt::UserRole + 300 // Should be enough for now
}; };
@ -191,6 +292,11 @@ public:
JFRN(mediaType), JFRN(mediaType),
JFRN(type), JFRN(type),
JFRN(collectionType), JFRN(collectionType),
JFRN(indexNumber),
JFRN(runTimeTicks),
JFRN(artists),
JFRN(isFolder),
JFRN(parentIndexNumber),
}; };
} }
QVariant data(const QModelIndex &index, int role) const override; QVariant data(const QModelIndex &index, int role) const override;

View file

@ -91,6 +91,7 @@ public:
Q_PROPERTY(QMediaPlayer::Error error READ error NOTIFY errorChanged) Q_PROPERTY(QMediaPlayer::Error error READ error NOTIFY errorChanged)
Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged) Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged)
Q_PROPERTY(bool hasVideo READ hasVideo NOTIFY hasVideoChanged) Q_PROPERTY(bool hasVideo READ hasVideo NOTIFY hasVideoChanged)
Q_PROPERTY(bool seekable READ seekable NOTIFY seekableChanged)
Q_PROPERTY(QObject* mediaObject READ mediaObject NOTIFY mediaObjectChanged) Q_PROPERTY(QObject* mediaObject READ mediaObject NOTIFY mediaObjectChanged)
Q_PROPERTY(QMediaPlayer::MediaStatus mediaStatus READ mediaStatus NOTIFY mediaStatusChanged) Q_PROPERTY(QMediaPlayer::MediaStatus mediaStatus READ mediaStatus NOTIFY mediaStatusChanged)
Q_PROPERTY(QMediaPlayer::State playbackState READ playbackState NOTIFY playbackStateChanged) Q_PROPERTY(QMediaPlayer::State playbackState READ playbackState NOTIFY playbackStateChanged)
@ -108,9 +109,10 @@ public:
int queueIndex() const { return m_queueIndex; } int queueIndex() const { return m_queueIndex; }
// Current media player related property getters // Current media player related property getters
QMediaPlayer::State playbackState() const { return m_playbackState; } QMediaPlayer::State playbackState() const { return m_mediaPlayer->state()/*m_playbackState*/; }
QMediaPlayer::MediaStatus mediaStatus() const { return m_mediaPlayer->mediaStatus(); } QMediaPlayer::MediaStatus mediaStatus() const { return m_mediaPlayer->mediaStatus(); }
bool hasVideo() const { return m_mediaPlayer->isVideoAvailable(); } bool hasVideo() const { return m_mediaPlayer->isVideoAvailable(); }
bool seekable() const { return m_mediaPlayer->isSeekable(); }
QMediaPlayer::Error error () const { return m_mediaPlayer->error(); } QMediaPlayer::Error error () const { return m_mediaPlayer->error(); }
QString errorString() const { return m_mediaPlayer->errorString(); } QString errorString() const { return m_mediaPlayer->errorString(); }
signals: signals:
@ -132,6 +134,7 @@ signals:
void playbackStateChanged(QMediaPlayer::State newState); void playbackStateChanged(QMediaPlayer::State newState);
void mediaStatusChanged(QMediaPlayer::MediaStatus newMediaStatus); void mediaStatusChanged(QMediaPlayer::MediaStatus newMediaStatus);
void hasVideoChanged(bool newHasVideo); void hasVideoChanged(bool newHasVideo);
void seekableChanged(bool newSeekable);
void errorChanged(QMediaPlayer::Error newError); void errorChanged(QMediaPlayer::Error newError);
void errorStringChanged(const QString &newErrorString); void errorStringChanged(const QString &newErrorString);
public slots: public slots:

View file

@ -0,0 +1,81 @@
/*
* Sailfin: a Jellyfin client written using Qt
* Copyright (C) 2021 Chris Josten and the Sailfin Contributors.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef JELLYFIN_VIEWMODEL_USERDATA_H
#define JELLYFIN_VIEWMODEL_USERDATA_H
#include <QDateTime>
#include <QObject>
#include <QSharedPointer>
#include "../dto/useritemdatadto.h"
namespace Jellyfin {
namespace ViewModel{
class UserData : public QObject {
Q_OBJECT
public:
explicit UserData(QObject* parent = nullptr);
explicit UserData(QSharedPointer<DTO::UserItemDataDto> data, QObject* parent = nullptr);
void setData(QSharedPointer<DTO::UserItemDataDto> data);
Q_PROPERTY(double rating READ rating NOTIFY ratingChanged)
Q_PROPERTY(double playedPercentage READ playedPercentage NOTIFY playedPercentageChanged)
Q_PROPERTY(int unplayedItemCount READ unplayedItemCount NOTIFY unplayedItemCountChanged)
Q_PROPERTY(qint64 playbackPositionTicks READ playbackPositionTicks NOTIFY playbackPositionTicksChanged);
Q_PROPERTY(int playCount READ playCount NOTIFY playCountChanged)
Q_PROPERTY(bool favorite READ favorite NOTIFY favoriteChanged)
Q_PROPERTY(bool m_likes READ likes NOTIFY likesChanged)
Q_PROPERTY(QDateTime lastPlayedDate READ lastPlayedDate NOTIFY lastPlayedDateChanged)
Q_PROPERTY(bool played READ played NOTIFY playedChanged)
Q_PROPERTY(QString key READ key NOTIFY keyChanged)
double rating() const { return m_data->rating().value_or(0); }
double playedPercentage() const { return m_data->playedPercentage().value_or(0); }
int unplayedItemCount() const { return m_data->unplayedItemCount().value_or(0); }
qint64 playbackPositionTicks() const { return m_data->playbackPositionTicks(); }
int playCount() const { return m_data->playCount(); }
bool favorite() const { return m_data->isFavorite(); }
bool likes() const { return m_data->likes().value_or(false); }
QDateTime lastPlayedDate() const { return m_data->lastPlayedDate(); }
bool played() const { return m_data->played(); }
QString key() const { return m_data->key(); }
signals:
void ratingChanged(double newRating);
void playedPercentageChanged(double newPlayedPercentage);
void unplayedItemCountChanged(int newUnplayedItemCount);
void playbackPositionTicksChanged(qint64 newPlaybackPositionTicks);
void playCountChanged(int newPlayCount);
void favoriteChanged(bool newFavorite);
void likesChanged(bool newLikes);
void lastPlayedDateChanged(QDateTime newLastPlayedDate);
void playedChanged(bool newPLayed);
void keyChanged(QString newKey);
private:
QSharedPointer<DTO::UserItemDataDto> m_data;
};
} // NS ViewModel
} // NS Jellyfin
#endif // JELLYFIN_VIEWMODEL_USERDATA_H

View file

@ -35,6 +35,7 @@ ApiClient::ApiClient(QObject *parent)
m_credManager = CredentialsManager::newInstance(this); m_credManager = CredentialsManager::newInstance(this);
connect(m_credManager, &CredentialsManager::serversListed, this, &ApiClient::credManagerServersListed); connect(m_credManager, &CredentialsManager::serversListed, this, &ApiClient::credManagerServersListed);
connect(m_credManager, &CredentialsManager::usersListed, this, &ApiClient::credManagerUsersListed); connect(m_credManager, &CredentialsManager::usersListed, this, &ApiClient::credManagerUsersListed);
connect(m_credManager, &CredentialsManager::tokenRetrieved, this, &ApiClient::credManagerTokenRetrieved);
generateDeviceProfile(); generateDeviceProfile();
} }
@ -118,20 +119,14 @@ void ApiClient::credManagerUsersListed(const QString &server, QStringList users)
QString user = users[0]; QString user = users[0];
qDebug() << "Chosen user: " << user; qDebug() << "Chosen user: " << user;
QObject *ctx3 = new QObject(this);
connect(m_credManager, &CredentialsManager::tokenRetrieved, ctx3, [this, ctx3]
(const QString &server, const QString &user, const QString &token) {
Q_UNUSED(server)
this->m_token = token;
this->setUserId(user);
this->setAuthenticated(true);
this->postCapabilities();
disconnect(ctx3);
}, Qt::UniqueConnection);
m_credManager->get(server, user); m_credManager->get(server, user);
} }
void ApiClient::credManagerTokenRetrieved(const QString &server, const QString &user, const QString &token) { void ApiClient::credManagerTokenRetrieved(const QString &server, const QString &user, const QString &token) {
this->m_token = token;
qDebug() << "Token retreived, logged in as user " << user;
this->setUserId(user);
this->setAuthenticated(true);
this->postCapabilities();
} }
void ApiClient::setupConnection() { void ApiClient::setupConnection() {

View file

@ -71,6 +71,10 @@ void BaseModelLoader::setAutoReload(bool newAutoReload) {
if (m_autoReload != newAutoReload) { if (m_autoReload != newAutoReload) {
m_autoReload = newAutoReload; m_autoReload = newAutoReload;
emit autoReloadChanged(newAutoReload); emit autoReloadChanged(newAutoReload);
if (canReload()) {
reload();
}
} }
} }

View file

@ -42,11 +42,6 @@ QString FallbackCredentialsManager::urlToGroupName(const QString &url) const {
return QString::number(qHash(url), 16); return QString::number(qHash(url), 16);
} }
QString FallbackCredentialsManager::groupNameToUrl(const QString &group) const {
QString tmp = QString(group);
return tmp.replace('|', "/");
}
void FallbackCredentialsManager::store(const QString &server, const QString &user, const QString &token) { void FallbackCredentialsManager::store(const QString &server, const QString &user, const QString &token) {
m_settings.setValue(urlToGroupName(server) + "/users/" + user + "/accessToken", token); m_settings.setValue(urlToGroupName(server) + "/users/" + user + "/accessToken", token);
m_settings.setValue(urlToGroupName(server) + "/address", server); m_settings.setValue(urlToGroupName(server) + "/address", server);

View file

@ -38,11 +38,15 @@ void registerTypes(const char *uri) {
qmlRegisterType<ViewModel::LatestMediaLoader>(uri, 1, 0, "LatestMediaLoader"); qmlRegisterType<ViewModel::LatestMediaLoader>(uri, 1, 0, "LatestMediaLoader");
qmlRegisterType<ViewModel::UserItemsLoader>(uri, 1, 0, "UserItemsLoader"); qmlRegisterType<ViewModel::UserItemsLoader>(uri, 1, 0, "UserItemsLoader");
qmlRegisterType<ViewModel::UserViewsLoader>(uri, 1, 0, "UsersViewsLoader"); qmlRegisterType<ViewModel::UserViewsLoader>(uri, 1, 0, "UsersViewsLoader");
qmlRegisterType<ViewModel::ResumeItemsLoader>(uri, 1, 0, "ResumeItemsLoader");
qmlRegisterType<ViewModel::ShowSeasonsLoader>(uri, 1, 0, "ShowSeasonsLoader");
qmlRegisterType<ViewModel::ShowEpisodesLoader>(uri, 1, 0, "ShowEpisodesLoader");
// Enumerations // Enumerations
qmlRegisterUncreatableType<Jellyfin::DTO::GeneralCommandTypeClass>(uri, 1, 0, "GeneralCommandType", "Is an enum"); qmlRegisterUncreatableType<Jellyfin::DTO::GeneralCommandTypeClass>(uri, 1, 0, "GeneralCommandType", "Is an enum");
qmlRegisterUncreatableType<Jellyfin::ViewModel::ModelStatusClass>(uri, 1, 0, "ModelStatus", "Is an enum"); qmlRegisterUncreatableType<Jellyfin::ViewModel::ModelStatusClass>(uri, 1, 0, "ModelStatus", "Is an enum");
qmlRegisterUncreatableType<Jellyfin::DTO::PlayMethodClass>(uri, 1, 0, "PlayMethod", "Is an enum"); qmlRegisterUncreatableType<Jellyfin::DTO::PlayMethodClass>(uri, 1, 0, "PlayMethod", "Is an enum");
qmlRegisterUncreatableType<Jellyfin::DTO::ItemFieldsClass>(uri, 1, 0, "ItemFields", "Is an enum");
qRegisterMetaType<Jellyfin::DTO::PlayMethodClass::Value>(); qRegisterMetaType<Jellyfin::DTO::PlayMethodClass::Value>();
} }

View file

@ -23,25 +23,14 @@ namespace Jellyfin {
namespace Model { namespace Model {
Item::Item() Item::Item(QObject *parent)
: Item(DTO::BaseItemDto(), nullptr) { } : Item(DTO::BaseItemDto(), nullptr, parent) { }
Item::Item(const DTO::BaseItemDto &data, ApiClient *apiClient) Item::Item(const DTO::BaseItemDto &data, ApiClient *apiClient, QObject *parent)
: DTO::BaseItemDto(data), m_apiClient(apiClient) { : DTO::BaseItemDto(data),
if (m_apiClient != nullptr) { QObject(parent),
m_apiClientConnections.append(QObject::connect( m_apiClient(nullptr) {
m_apiClient->eventbus(), setApiClient(apiClient);
&EventBus::itemUserDataUpdated,
[&](auto itemId, auto userData) {
onUserDataUpdated(itemId, userData);
}));
}
}
Item::~Item() {
for(auto it = m_apiClientConnections.begin(); it != m_apiClientConnections.end(); it++) {
QObject::disconnect(*it);
}
} }
bool Item::sameAs(const DTO::BaseItemDto &other) { bool Item::sameAs(const DTO::BaseItemDto &other) {
@ -50,23 +39,20 @@ bool Item::sameAs(const DTO::BaseItemDto &other) {
void Item::setApiClient(ApiClient *apiClient) { void Item::setApiClient(ApiClient *apiClient) {
if (m_apiClient != nullptr) { if (m_apiClient != nullptr) {
for(auto it = m_apiClientConnections.begin(); it != m_apiClientConnections.end(); it++) { disconnect(m_apiClient->eventbus(), &EventBus::itemUserDataUpdated,
QObject::disconnect(*it); this, &Item::updateUserData);
}
m_apiClientConnections.clear();
} }
this->m_apiClient = apiClient; this->m_apiClient = apiClient;
if (apiClient != nullptr) { if (apiClient != nullptr) {
m_apiClientConnections.append(QObject::connect( QObject::connect(m_apiClient->eventbus(), &EventBus::itemUserDataUpdated,
m_apiClient->eventbus(), this, &Item::updateUserData);
&EventBus::itemUserDataUpdated,
[&](auto itemId, auto userData) {
onUserDataUpdated(itemId, userData);
}));
} }
} }
void Item::onUserDataUpdated(const QString &itemId, const DTO::UserItemDataDto &userData) {
void Item::updateUserData(const QString &itemId, const DTO::UserItemDataDto &userData) {
if (itemId == this->jellyfinId()) {
emit userDataChanged(userData);
}
} }
} }

View file

@ -103,12 +103,12 @@ QSharedPointer<Item> Playlist::nextItem() {
return m_nextItem; return m_nextItem;
} }
void Playlist::appendToList(const ViewModel::ItemModel &model) { void Playlist::appendToList(ViewModel::ItemModel &model) {
int start = m_list.size(); int start = m_list.size();
int count = model.size(); int count = model.size();
m_list.reserve(count); m_list.reserve(count);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
m_list.append(QSharedPointer<Model::Item>::create(model.at(i))); m_list.append(QSharedPointer<Model::Item>(model.at(i)));
} }
emit itemsAddedToList(start, count); emit itemsAddedToList(start, count);
reshuffle(); reshuffle();

View file

@ -17,17 +17,39 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
#include "JellyfinQt/viewmodel/item.h" #include "JellyfinQt/viewmodel/item.h"
#include "JellyfinQt/viewmodel/userdata.h"
namespace Jellyfin { namespace Jellyfin {
namespace ViewModel { namespace ViewModel {
Item::Item(QObject *parent, QSharedPointer<Model::Item> data) Item::Item(QObject *parent, QSharedPointer<Model::Item> data)
: QObject(parent), m_data(data){ : QObject(parent),
m_data(data),
m_userData(new UserData(this)){
connect(m_data.data(), &Model::Item::userDataChanged, this, &Item::onUserDataChanged);
} }
void Item::setData(QSharedPointer<Model::Item> newData) { void Item::setData(QSharedPointer<Model::Item> newData) {
if (!m_data.isNull()) {
disconnect(m_data.data(), &Model::Item::userDataChanged, this, &Item::onUserDataChanged);
}
m_data = newData; m_data = newData;
if (!m_data.isNull()) {
connect(m_data.data(), &Model::Item::userDataChanged, this, &Item::onUserDataChanged);
}
}
void Item::setUserData(DTO::UserItemDataDto &newData) {
setUserData(QSharedPointer<DTO::UserItemDataDto>::create(newData));
}
void Item::setUserData(QSharedPointer<DTO::UserItemDataDto> newData) {
m_userData->setData(newData);
emit userDataChanged(m_userData);
}
void Item::onUserDataChanged(const DTO::UserItemDataDto &newData) {
setUserData(QSharedPointer<DTO::UserItemDataDto>::create(newData));
} }

View file

@ -18,12 +18,15 @@
*/ */
#include "JellyfinQt/viewmodel/itemmodel.h" #include "JellyfinQt/viewmodel/itemmodel.h"
#include "JellyfinQt/loader/http/getepisodes.h"
#include "JellyfinQt/loader/http/getlatestmedia.h" #include "JellyfinQt/loader/http/getlatestmedia.h"
#include "JellyfinQt/loader/http/getitemsbyuserid.h" #include "JellyfinQt/loader/http/getitemsbyuserid.h"
#include "JellyfinQt/loader/http/getresumeitems.h"
#include "JellyfinQt/loader/http/getseasons.h"
#define JF_CASE(roleName) case roleName: \ #define JF_CASE(roleName) case roleName: \
try { \ try { \
return QVariant(item.roleName()); \ return QVariant(item->roleName()); \
} catch(std::bad_optional_access &e) { \ } catch(std::bad_optional_access &e) { \
return QVariant(); \ return QVariant(); \
} }
@ -41,6 +44,15 @@ LatestMediaLoader::LatestMediaLoader(QObject *parent)
UserItemsLoader::UserItemsLoader(QObject *parent) UserItemsLoader::UserItemsLoader(QObject *parent)
: UserItemsLoaderBase(new Jellyfin::Loader::HTTP::GetItemsByUserIdLoader(), parent) {} : UserItemsLoaderBase(new Jellyfin::Loader::HTTP::GetItemsByUserIdLoader(), parent) {}
ResumeItemsLoader::ResumeItemsLoader(QObject *parent)
: ResumeItemsLoaderBase(new Jellyfin::Loader::HTTP::GetResumeItemsLoader(), parent) {}
ShowSeasonsLoader::ShowSeasonsLoader(QObject *parent)
: ShowSeasonsLoaderBase(new Jellyfin::Loader::HTTP::GetSeasonsLoader(), parent) {}
ShowEpisodesLoader::ShowEpisodesLoader(QObject *parent)
: ShowEpisodesLoaderBase(new Jellyfin::Loader::HTTP::GetEpisodesLoader(), parent) {}
ItemModel::ItemModel(QObject *parent) ItemModel::ItemModel(QObject *parent)
: ApiModel<Model::Item>(parent) { } : ApiModel<Model::Item>(parent) { }
@ -48,7 +60,7 @@ QVariant ItemModel::data(const QModelIndex &index, int role) const {
if (role <= Qt::UserRole || !index.isValid()) return QVariant(); if (role <= Qt::UserRole || !index.isValid()) return QVariant();
int row = index.row(); int row = index.row();
if (row < 0 || row >= m_array.size()) return QVariant(); if (row < 0 || row >= m_array.size()) return QVariant();
Model::Item item = m_array[row]; QSharedPointer<Model::Item> item = m_array[row];
switch(role) { switch(role) {
JF_CASE(jellyfinId) JF_CASE(jellyfinId)
JF_CASE(name) JF_CASE(name)
@ -66,6 +78,15 @@ QVariant ItemModel::data(const QModelIndex &index, int role) const {
JF_CASE(mediaType) JF_CASE(mediaType)
JF_CASE(type) JF_CASE(type)
JF_CASE(collectionType) JF_CASE(collectionType)
case RoleNames::indexNumber:
return QVariant(item->indexNumber().value_or(0));
case RoleNames::runTimeTicks:
return QVariant(item->runTimeTicks().value_or(0));
JF_CASE(artists)
case RoleNames::isFolder:
return QVariant(item->isFolder().value_or(false));
case RoleNames::parentIndexNumber:
return QVariant(item->parentIndexNumber().value_or(1));
default: default:
return QVariant(); return QVariant();
} }
@ -73,7 +94,7 @@ QVariant ItemModel::data(const QModelIndex &index, int role) const {
} }
QSharedPointer<Model::Item> ItemModel::itemAt(int index) { QSharedPointer<Model::Item> ItemModel::itemAt(int index) {
return QSharedPointer<Model::Item>::create(m_array[index]); return m_array[index];
} }
} // NS ViewModel } // NS ViewModel

View file

@ -56,6 +56,7 @@ PlaybackManager::PlaybackManager(QObject *parent)
connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::mediaPlayerDurationChanged); connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::mediaPlayerDurationChanged);
connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged); connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
connect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged); connect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged);
connect(m_mediaPlayer, &QMediaPlayer::seekableChanged, this, &PlaybackManager::seekableChanged);
// I do not like the complicated overload cast // I do not like the complicated overload cast
connect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::Error)), this, SLOT(mediaPlayerError(QMediaPlayer::Error))); connect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::Error)), this, SLOT(mediaPlayerError(QMediaPlayer::Error)));
@ -142,6 +143,7 @@ void PlaybackManager::mediaPlayerStateChanged(QMediaPlayer::State newState) {
postPlaybackInfo(Progress); postPlaybackInfo(Progress);
} }
m_oldState = newState; m_oldState = newState;
emit playbackStateChanged(newState);
} }
void PlaybackManager::mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus) { void PlaybackManager::mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus) {
@ -352,6 +354,9 @@ void ItemUrlFetcherThread::run() {
playMethod = PlayMethod::DirectPlay; playMethod = PlayMethod::DirectPlay;
} else if (source.supportsDirectStream()) { } else if (source.supportsDirectStream()) {
QString mediaType = item->mediaType(); QString mediaType = item->mediaType();
if (mediaType == "Video") {
mediaType.append('s');
}
QUrlQuery query; QUrlQuery query;
query.addQueryItem("mediaSourceId", source.jellyfinId()); query.addQueryItem("mediaSourceId", source.jellyfinId());
query.addQueryItem("deviceId", m_parent->m_apiClient->deviceId()); query.addQueryItem("deviceId", m_parent->m_apiClient->deviceId());

View file

@ -0,0 +1,39 @@
/*
* Sailfin: a Jellyfin client written using Qt
* Copyright (C) 2021 Chris Josten and the Sailfin Contributors.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <JellyfinQt/viewmodel/userdata.h>
namespace Jellyfin {
namespace ViewModel {
UserData::UserData(QObject *parent)
: QObject(parent),
m_data(QSharedPointer<DTO::UserItemDataDto>::create()) {
}
UserData::UserData(QSharedPointer<DTO::UserItemDataDto> data, QObject *parent)
: QObject(parent),
m_data(data) {
}
void UserData::setData(QSharedPointer<DTO::UserItemDataDto> data) {
m_data = data;
}
} // NS ViewModel
} // NS Jellyfin

View file

@ -18,7 +18,6 @@ set(harbour-sailfin_SOURCES
src/harbour-sailfin.cpp) src/harbour-sailfin.cpp)
set(sailfin_QML_SOURCES set(sailfin_QML_SOURCES
qml/ApiClient.qml
qml/Constants.qml qml/Constants.qml
qml/Utils.js qml/Utils.js
qml/components/music/NarrowAlbumCover.qml qml/components/music/NarrowAlbumCover.qml
@ -27,8 +26,8 @@ set(sailfin_QML_SOURCES
qml/components/videoplayer/VideoError.qml qml/components/videoplayer/VideoError.qml
qml/components/videoplayer/VideoHud.qml qml/components/videoplayer/VideoHud.qml
qml/components/IconListItem.qml qml/components/IconListItem.qml
qml/components/JItem.qml qml/components/JItem.qml
qml/components/LibraryItemDelegate.qml qml/components/LibraryItemDelegate.qml
qml/components/MoreSection.qml qml/components/MoreSection.qml
qml/components/PlainLabel.qml qml/components/PlainLabel.qml
qml/components/PlaybackBar.qml qml/components/PlaybackBar.qml
@ -60,8 +59,8 @@ set(sailfin_QML_SOURCES
qml/pages/itemdetails/VideoPage.qml qml/pages/itemdetails/VideoPage.qml
qml/pages/settings/DebugPage.qml qml/pages/settings/DebugPage.qml
qml/pages/setup/AddServerConnectingPage.qml qml/pages/setup/AddServerConnectingPage.qml
qml/pages/setup/AddServerPage.qml qml/pages/setup/AddServerPage.qml
qml/pages/setup/LoginDialog.qml qml/pages/setup/LoginDialog.qml
qml/qmldir) qml/qmldir)
add_executable(harbour-sailfin ${harbour-sailfin_SOURCES} ${sailfin_QML_SOURCES}) add_executable(harbour-sailfin ${harbour-sailfin_SOURCES} ${sailfin_QML_SOURCES})

View file

@ -3,7 +3,7 @@ Type=Application
Version=1.1 Version=1.1
X-Nemo-Application-Type=silica-qt5 X-Nemo-Application-Type=silica-qt5
Icon=harbour-sailfin Icon=harbour-sailfin
Exec=harbour-sailfin Exec=harbour-sailfin --no-attempt-sandbox
Name=Sailfin Name=Sailfin

View file

@ -1,7 +0,0 @@
pragma Singleton
import QtQuick 2.6
import nl.netsoj.chris.Jellyfin 1.0 as J
J.ApiClient {
supportedCommands: [J.GeneralCommandType.Play, J.GeneralCommandType.DisplayContent, J.GeneralCommandType.DisplayMessage]
}

View file

@ -46,7 +46,7 @@ QtObject {
} }
} }
readonly property real libraryDelegatePosterHeight: libraryDelegateHeight * 1.6667 readonly property real libraryDelegatePosterHeight: libraryDelegateHeight * 1.5 // 1.6667
readonly property real libraryProgressHeight: Theme.paddingMedium readonly property real libraryProgressHeight: Theme.paddingMedium

View file

@ -21,7 +21,7 @@ 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 nl.netsoj.chris.Jellyfin 1.0 as J
import "../" import "../"
@ -30,7 +30,7 @@ import "../"
* +---+--------------------------------------+ * +---+--------------------------------------+
* |\ /| +---+ | * |\ /| +---+ |
* | \ / | Media title | | | * | \ / | Media title | | |
* | X | | | | * | X | | | |
* | / \ | Artist 1, artist 2 | | | * | / \ | Artist 1, artist 2 | | |
* |/ \| +---+ | * |/ \| +---+ |
* +-----+------------------------------------+ * +-----+------------------------------------+
@ -40,13 +40,15 @@ PanelBackground {
height: Theme.itemSizeLarge height: Theme.itemSizeLarge
width: parent.width width: parent.width
y: parent.height - height y: parent.height - height
property PlaybackManager manager //FIXME: Once QTBUG-10822 is resolved, change to J.PlaybackManager
property var manager
property bool open property bool open
property real visibleSize: height property real visibleSize: height
property bool isFullPage: false property bool isFullPage: false
property bool showQueue: false property bool showQueue: false
property bool _pageWasShowingNavigationIndicator property bool _pageWasShowingNavigationIndicator
readonly property bool mediaLoading: [MediaPlayer.Loading, MediaPlayer.Buffering].indexOf(manager.mediaStatus) >= 0
transform: Translate {id: playbackBarTranslate; y: 0} transform: Translate {id: playbackBarTranslate; y: 0}
@ -72,10 +74,11 @@ PanelBackground {
} }
source: largeAlbumArt.source source: largeAlbumArt.source
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
opacity: 1
Image { Image {
id: largeAlbumArt id: largeAlbumArt
source: Utils.itemImageUrl(ApiClient.baseUrl, manager.item, "Primary") source: Utils.itemImageUrl(apiClient.baseUrl, manager.item, "Primary")
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
anchors.fill: parent anchors.fill: parent
opacity: 0 opacity: 0
@ -119,11 +122,11 @@ PanelBackground {
Label { Label {
id: artists id: artists
text: { text: {
if (manager.item == null) return qsTr("Play some media!") //return manager.item.mediaType;
console.log(manager.item.type) if (manager.item === null) return qsTr("Play some media!")
switch(manager.item.type) { switch(manager.item.mediaType) {
case "Audio": case "Audio":
return manager.item.artists.join(", ") return manager.item.artists //.join(", ")
} }
return qsTr("Not audio") return qsTr("Not audio")
} }
@ -157,6 +160,7 @@ PanelBackground {
icon.source: "image://theme/icon-m-previous" icon.source: "image://theme/icon-m-previous"
enabled: false enabled: false
opacity: 0 opacity: 0
onClicked: manager.previous()
} }
IconButton { IconButton {
@ -182,6 +186,7 @@ PanelBackground {
icon.source: "image://theme/icon-m-next" icon.source: "image://theme/icon-m-next"
enabled: false enabled: false
opacity: 0 opacity: 0
onClicked: manager.next()
} }
IconButton { IconButton {
id: queueButton id: queueButton
@ -206,7 +211,7 @@ PanelBackground {
minimumValue: 0 minimumValue: 0
value: manager.position value: manager.position
maximumValue: manager.duration maximumValue: manager.duration
indeterminate: [MediaPlayer.Loading, MediaPlayer.Buffering].indexOf(manager.mediaStatus) >= 0 indeterminate: mediaLoading
} }
Slider { Slider {
@ -352,7 +357,7 @@ PanelBackground {
}, },
State { State {
name: "hidden" name: "hidden"
when: (manager.playbackState === MediaPlayer.StoppedState || "__hidePlaybackBar" in pageStack.currentPage) && !isFullPage when: ((manager.playbackState === MediaPlayer.StoppedState && !mediaLoading) || "__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

View file

@ -20,7 +20,7 @@ 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 nl.netsoj.chris.Jellyfin 1.0 as J
import "videoplayer" import "videoplayer"
import "../" import "../"
@ -31,7 +31,8 @@ import "../"
SilicaItem { SilicaItem {
id: playerRoot id: playerRoot
property JellyfinItem item //FIXME: Once QTBUG-10822 is resolved, change to J.Item
property var item
property string title: item.name property string title: item.name
property bool resume property bool resume
property int progress property int progress
@ -39,7 +40,8 @@ SilicaItem {
readonly property bool hudVisible: !hud.hidden || manager.error !== MediaPlayer.NoError readonly property bool hudVisible: !hud.hidden || manager.error !== MediaPlayer.NoError
property int audioTrack: 0 property int audioTrack: 0
property int subtitleTrack: 0 property int subtitleTrack: 0
property PlaybackManager manager; //FIXME: Once QTBUG-10822 is resolved, change to J.PlaybackManager
property var manager;
// Blackground to prevent the ambience from leaking through // Blackground to prevent the ambience from leaking through
Rectangle { Rectangle {
@ -59,21 +61,6 @@ SilicaItem {
manager: playerRoot.manager manager: playerRoot.manager
title: videoPlayer.title title: videoPlayer.title
Label {
anchors.fill: parent
anchors.margins: Theme.horizontalPageMargin
text: item.jellyfinId + "\n" + appWindow.playbackManager.streamUrl + "\n"
+ (manager.playMethod === PlaybackManager.DirectPlay ? "Direct Play" : "Transcoding") + "\n"
+ manager.position + "\n"
+ manager.mediaStatus + "\n"
// + player.bufferProgress + "\n"
// + player.metaData.videoCodec + "@" + player.metaData.videoFrameRate + "(" + player.metaData.videoBitRate + ")" + "\n"
// + player.metaData.audioCodec + "(" + player.metaData.audioBitRate + ")" + "\n"
// + player.errorString + "\n"
font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: "WordWrap"
visible: appWindow.showDebugInfo
}
} }
VideoError { VideoError {
@ -81,11 +68,35 @@ SilicaItem {
player: manager player: manager
} }
Label {
anchors.fill: parent
anchors.margins: Theme.horizontalPageMargin
text: item.jellyfinId + "\n" + appWindow.playbackManager.streamUrl + "\n"
+ (manager.playMethod === J.PlaybackManager.DirectPlay ? "Direct Play" : "Transcoding") + "\n"
+ manager.position + "\n"
+ manager.mediaStatus + "\n"
// + player.bufferProgress + "\n"
// + player.metaData.videoCodec + "@" + player.metaData.videoFrameRate + "(" + player.metaData.videoBitRate + ")" + "\n"
// + player.metaData.audioCodec + "(" + player.metaData.audioBitRate + ")" + "\n"
// + player.errorString + "\n"
font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: "WordWrap"
visible: appWindow.showDebugInfo
MouseArea {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
enabled: parent.visible
onClicked: Clipboard.text = appWindow.playbackManager.streamUrl
}
}
function start() { function start() {
manager.audioIndex = audioTrack manager.audioIndex = audioTrack
manager.subtitleIndex = subtitleTrack manager.subtitleIndex = subtitleTrack
manager.resumePlayback = resume manager.resumePlayback = resume
manager.playItem(item.jellyfinId) manager.playItem(item)
} }
function stop() { function stop() {

View file

@ -20,11 +20,12 @@ 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 import nl.netsoj.chris.Jellyfin 1.0 as J
Rectangle { Rectangle {
id: videoError id: videoError
property PlaybackManager player //FIXME: Once QTBUG-10822 is resolved, change to J.PlaybackManager
property var 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 {} }

View file

@ -20,7 +20,7 @@ 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 nl.netsoj.chris.Jellyfin 1.0 as J
import "../../Utils.js" as Utils import "../../Utils.js" as Utils
@ -30,7 +30,8 @@ import "../../Utils.js" as Utils
*/ */
Item { Item {
id: videoHud id: videoHud
property PlaybackManager manager //FIXME: Once QTBUG-10822 is resolved, change to J.PlaybackManager
property var 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

View file

@ -35,6 +35,7 @@ ApplicationWindow {
// 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
readonly property ApiClient apiClient: _apiClient
// Due QTBUG-10822, declarartions such as `property J.Item foo` are not possible. // Due QTBUG-10822, declarartions such as `property J.Item foo` are not possible.
property QtObject itemData property QtObject itemData
@ -46,20 +47,23 @@ ApplicationWindow {
property bool _hidePlaybackBar: false property bool _hidePlaybackBar: false
bottomMargin: playbackBar.visibleSize bottomMargin: playbackBar.visibleSize
ApiClient {
id: _apiClient
objectName: "Test"
supportedCommands: [J.GeneralCommandType.Play, J.GeneralCommandType.DisplayContent, J.GeneralCommandType.DisplayMessage]
}
initialPage: Component { initialPage: Component {
MainPage { MainPage {
Connections { Connections {
target: D.ApiClient target: apiClient
// Replace the MainPage if no server was set up. // Replace the MainPage if no server was set up.
} }
onStatusChanged: { onStatusChanged: {
if (status == PageStatus.Active && !_hasInitialized) { if (status == PageStatus.Active && !_hasInitialized) {
_hasInitialized = true; _hasInitialized = true;
D.ApiClient.restoreSavedSession(); apiClient.restoreSavedSession();
} }
} }
} }
@ -87,14 +91,9 @@ ApplicationWindow {
} }
} }
/*MediaPlayer {
id: _mediaPlayer
autoPlay: true
}*/
PlaybackManager { PlaybackManager {
id: _playbackManager id: _playbackManager
apiClient: D.ApiClient apiClient: appWindow.apiClient
audioIndex: 0 audioIndex: 0
autoOpen: true autoOpen: true
} }
@ -111,14 +110,13 @@ ApplicationWindow {
PlaybackBar { PlaybackBar {
id: playbackBar id: playbackBar
manager: _playbackManager manager: _playbackManager
state: "hidden"
// CTMBWSIU: Code That Might Break When Silica Is Updated // CTMBWSIU: Code That Might Break When Silica Is Updated
Component.onCompleted: playbackBar.parent = __silica_applicationwindow_instance._rotatingItem Component.onCompleted: playbackBar.parent = __silica_applicationwindow_instance._rotatingItem
} }
//FIXME: proper error handling //FIXME: proper error handling
Connections { Connections {
target: D.ApiClient target: apiClient
onNetworkError: errorNotification.show("Network error: " + error) onNetworkError: errorNotification.show("Network error: " + error)
onConnectionFailed: errorNotification.show("Connect error: " + error) onConnectionFailed: errorNotification.show("Connect error: " + error)
//onConnectionSuccess: errorNotification.show("Success: " + loginMessage) //onConnectionSuccess: errorNotification.show("Success: " + loginMessage)

View file

@ -54,7 +54,7 @@ Page {
"Copyright © Chris Josten 2020</p>" + "Copyright © Chris Josten 2020</p>" +
"<p>Sailfin is Free Software licensed under the <a href='lgpl'>LGPL-v2.1</a> or later, at your choice. " + "<p>Sailfin is Free Software licensed under the <a href='lgpl'>LGPL-v2.1</a> or later, at your choice. " +
"Parts of the code of Sailfin are from other libraries. <a href='3rdparty'>View their licenses here</a>.</p>") "Parts of the code of Sailfin are from other libraries. <a href='3rdparty'>View their licenses here</a>.</p>")
.arg(ApiClient.version) .arg(apiClient.version)
textFormat: Text.StyledText textFormat: Text.StyledText
color: Theme.secondaryHighlightColor color: Theme.secondaryHighlightColor
linkColor: Theme.primaryColor linkColor: Theme.primaryColor

View file

@ -23,7 +23,6 @@ import nl.netsoj.chris.Jellyfin 1.0 as J
import "../components" import "../components"
import "../" import "../"
import "../Utils.js" as Utils
/** /**
* Main page, which simply shows some content of every library, as well as next items. * Main page, which simply shows some content of every library, as well as next items.
@ -49,7 +48,7 @@ Page {
text: qsTr("Reload") text: qsTr("Reload")
onClicked: loadModels(true) onClicked: loadModels(true)
} }
busy: userViewsLoader.status === J.UsersViewsLoader.Loading busy: mediaLibraryLoader.status === J.UsersViewsLoader.Loading
} }
} }
@ -73,7 +72,7 @@ Page {
id: mediaLibraryModel id: mediaLibraryModel
loader: J.UsersViewsLoader { loader: J.UsersViewsLoader {
id: mediaLibraryLoader id: mediaLibraryLoader
apiClient: ApiClient apiClient: appWindow.apiClient
} }
} }
@ -81,7 +80,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 === J.ApiModel.Loading busy: userResumeLoader.status === J.ApiModel.Loading
Loader { Loader {
width: parent.width width: parent.width
sourceComponent: carrouselView sourceComponent: carrouselView
@ -90,10 +89,12 @@ Page {
J.ItemModel { J.ItemModel {
id: userResumeModel id: userResumeModel
// Resume model loader: J.ResumeItemsLoader {
/*apiClient: ApiClient id: userResumeLoader
limit: 12 apiClient: appWindow.apiClient
recursive: true*/ limit: 12
//recursive: true*/
}
} }
} }
} }
@ -111,7 +112,7 @@ Page {
J.ItemModel { J.ItemModel {
id: showNextUpModel id: showNextUpModel
/*apiClient: ApiClient /*apiClient: appWindow.apiClient
limit: 12*/ limit: 12*/
} }
} }
@ -132,7 +133,7 @@ Page {
J.ItemModel { J.ItemModel {
id: userItemModel id: userItemModel
loader: J.LatestMediaLoader { loader: J.LatestMediaLoader {
apiClient: ApiClient apiClient: appWindow.apiClient
parentId: jellyfinId parentId: jellyfinId
} }
} }
@ -171,7 +172,7 @@ Page {
} }
Connections { Connections {
target: ApiClient target: appWindow.apiClient
onAuthenticatedChanged: loadModels(false) onAuthenticatedChanged: loadModels(false)
} }
@ -181,7 +182,7 @@ Page {
* even if loaded. * even if loaded.
*/ */
function loadModels(force) { function loadModels(force) {
if (force || (ApiClient.authenticated && !_modelsLoaded)) { if (force || (appWindow.apiClient.authenticated && !_modelsLoaded)) {
_modelsLoaded = true; _modelsLoaded = true;
mediaLibraryModel.reload() mediaLibraryModel.reload()
//userResumeModel.reload() //userResumeModel.reload()
@ -192,14 +193,15 @@ Page {
Component { Component {
id: carrouselView id: carrouselView
SilicaListView { SilicaListView {
property bool isPortrait: Utils.usePortraitCover(collectionType)
id: list id: list
clip: true clip: true
height: { height: {
if (count > 0) { if (count > 0) {
if (["tvshows", "movies"].indexOf(collectionType) == -1) { if (isPortrait) {
Constants.libraryDelegateHeight
} else {
Constants.libraryDelegatePosterHeight Constants.libraryDelegatePosterHeight
} else {
Constants.libraryDelegateHeight
} }
} else { } else {
0 0
@ -217,12 +219,12 @@ Page {
delegate: LibraryItemDelegate { delegate: LibraryItemDelegate {
property string id: model.jellyfinId property string id: model.jellyfinId
title: model.name title: model.name
poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.jellyfinId, model.imageTags["Primary"], "Primary", {"maxHeight": height}) poster: Utils.itemModelImageUrl(appWindow.apiClient.baseUrl, model.jellyfinId, model.imageTags["Primary"], "Primary", {"maxHeight": height})
Binding on blurhash { Binding on blurhash {
when: poster !== "" when: poster !== ""
value: model.imageBlurHashes["Primary"][model.imageTags["Primary"]] value: model.imageBlurHashes["Primary"][model.imageTags["Primary"]]
} }
landscape: !Utils.usePortraitCover(collectionType) landscape: !isPortrait
progress: (typeof model.userData !== "undefined") ? model.userData.playedPercentage / 100 : 0.0 progress: (typeof model.userData !== "undefined") ? model.userData.playedPercentage / 100 : 0.0
onClicked: { onClicked: {

View file

@ -22,6 +22,7 @@ import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J import nl.netsoj.chris.Jellyfin 1.0 as J
import "../components" import "../components"
import ".."
Page { Page {
id: settingsPage id: settingsPage
@ -69,7 +70,7 @@ Page {
top: parent.top top: parent.top
bottom: parent.bottom bottom: parent.bottom
} }
source: ApiClient.baseUrl + "/Users/" + ApiClient.userId + "/Images/Primary?tag=" + loggedInUser.primaryImageTag source: apiClient.baseUrl + "/Users/" + apiClient.userId + "/Images/Primary?tag=" + loggedInUser.primaryImageTag
} }
Label { Label {
@ -80,7 +81,7 @@ Page {
bottom: parent.verticalCenter bottom: parent.verticalCenter
right: parent.right right: parent.right
} }
text: loggedInUser.status == User.Ready ? loggedInUser.name : ApiClient.userId text: loggedInUser.status == User.Ready ? loggedInUser.name : apiClient.userId
color: Theme.highlightColor color: Theme.highlightColor
} }
@ -92,7 +93,7 @@ Page {
top: parent.verticalCenter top: parent.verticalCenter
right: parent.right right: parent.right
} }
text: ApiClient.baseUrl text: apiClient.baseUrl
color: Theme.secondaryHighlightColor color: Theme.secondaryHighlightColor
} }
@ -104,21 +105,23 @@ Page {
ButtonLayout { ButtonLayout {
Button { Button {
text: qsTr("Log out") text: qsTr("Log out")
onClicked: remorse.execute(qsTr("Logging out"), ApiClient.deleteSession) onClicked: remorse.execute(qsTr("Logging out"), apiClient.deleteSession)
} }
} }
SectionHeader { SectionHeader {
//: Other settings //: Other settings menu item
text: qsTr("Other") text: qsTr("Other")
} }
IconListItem { IconListItem {
//: Debug information settings menu itemy
text: qsTr("Debug information") text: qsTr("Debug information")
iconSource: "image://theme/icon-s-developer" iconSource: "image://theme/icon-s-developer"
onClicked: pageStack.push(Qt.resolvedUrl("settings/DebugPage.qml")) onClicked: pageStack.push(Qt.resolvedUrl("settings/DebugPage.qml"))
} }
//: About Sailfin settings menu itemy
IconListItem { IconListItem {
text: qsTr("About Sailfin") text: qsTr("About Sailfin")
iconSource: "image://theme/icon-m-about" iconSource: "image://theme/icon-m-about"

View file

@ -21,7 +21,7 @@ import Sailfish.Silica 1.0
import "../components" import "../components"
import nl.netsoj.chris.Jellyfin 1.0 import nl.netsoj.chris.Jellyfin 1.0 as J
/** /**
* Page only containing a video player. * Page only containing a video player.
@ -33,7 +33,7 @@ Page {
id: videoPage id: videoPage
// PlaybackBar will hide itself when it encounters a page with such a property // PlaybackBar will hide itself when it encounters a page with such a property
property bool __hidePlaybackBar: true property bool __hidePlaybackBar: true
property JellyfinItem itemData property var itemData
property int audioTrack property int audioTrack
property int subtitleTrack property int subtitleTrack
property bool resume: true property bool resume: true

View file

@ -22,7 +22,7 @@ import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J import nl.netsoj.chris.Jellyfin 1.0 as J
import "../../components" import "../../components"
import "../.." import "../../"
/** /**
* This page displays details about a film, show, season, episode, and so on. * This page displays details about a film, show, season, episode, and so on.
@ -34,8 +34,6 @@ Page {
id: pageRoot id: pageRoot
property string itemId: "" property string itemId: ""
property alias itemData: jItemLoader.data property alias itemData: jItemLoader.data
//property string itemId: ""
//property var itemData: ({})
property bool _loading: jItemLoader.status === J.ItemLoader.Loading property bool _loading: jItemLoader.status === J.ItemLoader.Loading
readonly property bool hasLogo: (typeof itemData.imageTags !== "undefined") && (typeof itemData.imageTags["Logo"] !== "undefined") readonly property bool hasLogo: (typeof itemData.imageTags !== "undefined") && (typeof itemData.imageTags["Logo"] !== "undefined")
property string _chosenBackdropImage: "" property string _chosenBackdropImage: ""
@ -46,10 +44,10 @@ Page {
if (itemData.backdropImageTags.length > 0) { if (itemData.backdropImageTags.length > 0) {
rand = Math.floor(Math.random() * (itemData.backdropImageTags.length - 0.001)) rand = Math.floor(Math.random() * (itemData.backdropImageTags.length - 0.001))
console.log("Random: ", rand) console.log("Random: ", rand)
_chosenBackdropImage = ApiClient.baseUrl + "/Items/" + itemId + "/Images/Backdrop/" + rand + "?tag=" +itemData.backdropImageTags[rand] + "&maxHeight" + height _chosenBackdropImage = apiClient.baseUrl + "/Items/" + itemId + "/Images/Backdrop/" + rand + "?tag=" +itemData.backdropImageTags[rand] + "&maxHeight" + height
} else if (itemData.parentBackdropImageTags.length > 0) { } else if (itemData.parentBackdropImageTags.length > 0) {
rand = Math.floor(Math.random() * (itemData.parentBackdropImageTags.length - 0.001)) rand = Math.floor(Math.random() * (itemData.parentBackdropImageTags.length - 0.001))
_chosenBackdropImage = ApiClient.baseUrl + "/Items/" + itemData.parentBackdropItemId + "/Images/Backdrop/" + rand + "?tag=" + itemData.parentBackdropImageTags[0] _chosenBackdropImage = apiClient.baseUrl + "/Items/" + itemData.parentBackdropItemId + "/Images/Backdrop/" + rand + "?tag=" + itemData.parentBackdropImageTags[0]
} }
} }
@ -86,8 +84,9 @@ Page {
J.ItemLoader { J.ItemLoader {
id: jItemLoader id: jItemLoader
apiClient: ApiClient apiClient: appWindow.apiClient
itemId: pageRoot.itemId itemId: pageRoot.itemId
autoReload: false
onStatusChanged: { onStatusChanged: {
console.log("Status changed: " + newStatus, JSON.stringify(jItemLoader.data)) console.log("Status changed: " + newStatus, JSON.stringify(jItemLoader.data))
if (status === J.ItemLoader.Ready) { if (status === J.ItemLoader.Ready) {
@ -96,15 +95,13 @@ Page {
} }
} }
Label {
text: "ItemLoader status=%1, \nitemId=%2\nitemData=%3".arg(jItemLoader.status).arg(jItemLoader.itemId).arg(jItemLoader.data)
}
onStatusChanged: { onStatusChanged: {
if (status == PageStatus.Deactivating) { if (status === PageStatus.Deactivating) {
//appWindow.itemData = ({}) //appWindow.itemData = ({})
} }
if (status == PageStatus.Active) { if (status === PageStatus.Active) {
console.log("Page ready, ItemID: ", itemId, ", UserID: ", apiClient.userId)
jItemLoader.autoReload = true
//appWindow.itemData = jItemLoader.data //appWindow.itemData = jItemLoader.data
} }
} }

View file

@ -29,10 +29,13 @@ BaseDetailPage {
J.ItemModel { J.ItemModel {
id: collectionModel id: collectionModel
//sortBy: ["SortName"]
loader: J.UserItemsLoader { loader: J.UserItemsLoader {
apiClient: ApiClient id: collectionLoader
apiClient: appWindow.apiClient
parentId: itemData.jellyfinId parentId: itemData.jellyfinId
autoReload: itemData.jellyfinId.length > 0
onParentIdChanged: if (parentId.length > 0) reload()
sortBy: "SortName"
} }
} }
@ -61,7 +64,7 @@ BaseDetailPage {
RemoteImage { RemoteImage {
id: itemImage id: itemImage
anchors.fill: parent anchors.fill: parent
source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.jellyfinId, 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
@ -138,20 +141,20 @@ BaseDetailPage {
MenuItem { MenuItem {
//: Sort order //: Sort order
text: qsTr("Ascending") text: qsTr("Ascending")
onClicked: apply(model.value, ApiModel.Ascending) onClicked: apply(model.value, "Ascending")
} }
MenuItem { MenuItem {
//: Sort order //: Sort order
text: qsTr("Descending") text: qsTr("Descending")
onClicked: apply(model.value, ApiModel.Descending) onClicked: apply(model.value, "Descending")
} }
} }
onClicked: openMenu() onClicked: openMenu()
function apply(field, order) { function apply(field, order) {
collectionModel.sortBy = [field]; collectionLoader.sortBy = field;
collectionModel.sortOrder = order; collectionLoader.sortOrder = order;
collectionModel.reload() collectionLoader.reload()
pageStack.pop() pageStack.pop()
} }
} }

View file

@ -36,11 +36,12 @@ BaseDetailPage {
J.ItemModel { J.ItemModel {
id: collectionModel id: collectionModel
loader: J.UserItemsLoader { loader: J.UserItemsLoader {
apiClient: ApiClient apiClient: appWindow.apiClient
//sortBy: ["SortName"] sortBy: "SortName"
//fields: ["ItemCounts","PrimaryImageAspectRatio","BasicSyncInfo","CanDelete","MediaSourceCount"] //fields: ["ItemCounts","PrimaryImageAspectRatio","BasicSyncInfo","CanDelete","MediaSourceCount"]
parentId: itemData.jellyfinId parentId: itemData.jellyfinId
onParentIdChanged: reload() autoReload: itemData.jellyfinId.length > 0
onParentIdChanged: if (parentId.length > 0) reload()
} }
} }
RowLayout { RowLayout {
@ -52,9 +53,9 @@ BaseDetailPage {
visible: _twoColumns visible: _twoColumns
Layout.minimumWidth: 1000 / Theme.pixelRatio Layout.minimumWidth: 1000 / Theme.pixelRatio
Layout.fillHeight: true Layout.fillHeight: true
/*source: visible source: visible
? "../../components/music/WideAlbumCover.qml" : "" ? "../../components/music/WideAlbumCover.qml" : ""
onLoaded: bindAlbum(item)*/ onLoaded: bindAlbum(item)
} }
Item {height: 1; width: Theme.horizontalPageMargin; visible: wideAlbumCover.visible; } Item {height: 1; width: Theme.horizontalPageMargin; visible: wideAlbumCover.visible; }
SilicaListView { SilicaListView {
@ -64,8 +65,8 @@ BaseDetailPage {
model: collectionModel model: collectionModel
header: Loader { header: Loader {
width: parent.width width: parent.width
/*source: "../../components/music/NarrowAlbumCover.qml" source: "../../components/music/NarrowAlbumCover.qml"
onLoaded: bindAlbum(item)*/ onLoaded: bindAlbum(item)
} }
section { section {
property: "parentIndexNumber" property: "parentIndexNumber"
@ -79,7 +80,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.jellyfinId) onClicked: window.playbackManager.playItemInList(collectionModel, model.index)
} }
VerticalScrollDecorator {} VerticalScrollDecorator {}
@ -87,7 +88,7 @@ BaseDetailPage {
} }
function bindAlbum(item) { function bindAlbum(item) {
//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 itemData.albumArtist}) item.albumArtist = Qt.binding(function() { return itemData.albumArtist})

View file

@ -1,7 +1,7 @@
import QtQuick 2.6 import QtQuick 2.6
import Sailfish.Silica 1.0 import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0 import nl.netsoj.chris.Jellyfin 1.0 as J
import "../../components" import "../../components"
@ -21,7 +21,7 @@ BaseDetailPage {
RemoteImage { RemoteImage {
id: image id: image
source: ApiClient.downloadUrl(itemId) source: apiClient.downloadUrl(itemId)
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
anchors.fill: parent anchors.fill: parent

View file

@ -19,19 +19,22 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import QtQuick 2.6 import QtQuick 2.6
import Sailfish.Silica 1.0 import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0 import nl.netsoj.chris.Jellyfin 1.0 as J
import "../.." import "../.."
import "../../components" import "../../components"
import ".." import ".."
BaseDetailPage { BaseDetailPage {
ShowEpisodesModel { J.ItemModel {
id: episodeModel id: episodeModel
apiClient: ApiClient loader: J.ShowEpisodesLoader {
show: itemData.seriesId apiClient: appWindow.apiClient
seasonId: itemData.jellyfinId seriesId: itemData.seriesId
fields: ["Overview"] seasonId: itemData.jellyfinId
fields: [J.ItemFields.Overview]
autoReload: itemData.jellyfinId.length > 0
}
} }
Connections { Connections {
@ -42,7 +45,7 @@ BaseDetailPage {
SilicaListView { SilicaListView {
anchors.fill: parent anchors.fill: parent
contentHeight: content.height contentHeight: content.height
visible: itemData.status !== JellyfinItem.Error //visible: itemData.status !== JellyfinItem.Error
header: PageHeader { header: PageHeader {
title: itemData.name title: itemData.name
@ -60,7 +63,7 @@ BaseDetailPage {
} }
width: Constants.libraryDelegateWidth width: Constants.libraryDelegateWidth
height: Constants.libraryDelegateHeight height: Constants.libraryDelegateHeight
source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.jellyfinId, 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] blurhash: model.imageBlurHashes.Primary[model.imageTags.Primary]
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
clip: true clip: true
@ -157,8 +160,8 @@ BaseDetailPage {
onStatusChanged: { onStatusChanged: {
if (status == PageStatus.Active) { if (status == PageStatus.Active) {
//console.log(JSON.stringify(itemData)) //console.log(JSON.stringify(itemData))
episodeModel.show = itemData.seriesId //episodeModel.show = itemData.seriesId
episodeModel.seasonId = itemData.jellyfinId //episodeModel.seasonId = itemData.jellyfinId
} }
} }
} }

View file

@ -19,7 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import QtQuick 2.6 import QtQuick 2.6
import Sailfish.Silica 1.0 import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0 import nl.netsoj.chris.Jellyfin 1.0 as J
import "../../components" import "../../components"
import "../.." import "../.."
@ -28,7 +28,7 @@ BaseDetailPage {
SilicaFlickable { SilicaFlickable {
anchors.fill: parent anchors.fill: parent
contentHeight: content.height contentHeight: content.height
visible: itemData.status !== JellyfinItem.Error //visible: itemData.status !== JellyfinItem.Error
Column { Column {
id: content id: content
@ -47,7 +47,7 @@ BaseDetailPage {
RemoteImage { RemoteImage {
id: logoImage id: logoImage
anchors.centerIn: parent anchors.centerIn: parent
source: Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Logo") source: Utils.itemImageUrl(apiClient.baseUrl, itemData, "Logo")
} }
} }
@ -63,11 +63,14 @@ BaseDetailPage {
text: qsTr("Seasons") text: qsTr("Seasons")
} }
ShowSeasonsModel { J.ItemModel {
id: showSeasonsModel id: showSeasonsModel
apiClient: ApiClient loader: J.ShowSeasonsLoader {
show: itemData.jellyfinId id: showSeasonLoader
onShowChanged: reload() apiClient: appWindow.apiClient
seriesId: itemData.jellyfinId
autoReload: itemData.jellyfinId.length > 0
}
} }
Connections { Connections {
target: itemData target: itemData
@ -84,7 +87,7 @@ BaseDetailPage {
leftMargin: Theme.horizontalPageMargin leftMargin: Theme.horizontalPageMargin
rightMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin
delegate: LibraryItemDelegate { delegate: LibraryItemDelegate {
poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.jellyfinId, 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.jellyfinId}) onClicked: pageStack.push(Utils.getPageUrl(model.mediaType, model.type), {"itemId": model.jellyfinId})

View file

@ -31,7 +31,7 @@ BaseDetailPage {
enabled: true enabled: true
text: qsTr("Item type (%1) unsupported").arg(itemData.type) text: qsTr("Item type (%1) unsupported").arg(itemData.type)
hintText: qsTr("This is still an alpha version :)") hintText: qsTr("Fallback page for %2 not found either\nThis is still an alpha version :)").arg(itemData.mediaType)
} }
} }
} }

View file

@ -55,7 +55,7 @@ BaseDetailPage {
PlayToolbar { PlayToolbar {
id: toolbar id: toolbar
width: parent.width width: parent.width
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"]]
Binding on favourited { Binding on favourited {

View file

@ -54,7 +54,7 @@ Page {
label: qsTr("Connection state") label: qsTr("Connection state")
value: { value: {
var stateText var stateText
switch(ApiClient.websocket.state) { switch(apiClient.websocket.state) {
case 0: case 0:
//- Socket state //- Socket state
stateText = qsTr("Unconnected"); stateText = qsTr("Unconnected");
@ -85,7 +85,7 @@ Page {
break; break;
} }
//- Socket state: "state no (state description)" //- Socket state: "state no (state description)"
qsTr("%1 (%2)").arg(ApiClient.websocket.state).arg(stateText) qsTr("%1 (%2)").arg(apiClient.websocket.state).arg(stateText)
} }
} }
@ -105,7 +105,7 @@ Page {
Label { Label {
id: deviceProfile id: deviceProfile
color: Theme.secondaryHighlightColor color: Theme.secondaryHighlightColor
text: JSON.stringify(ApiClient.deviceProfile, null, '\t') text: JSON.stringify(apiClient.deviceProfile, null, '\t')
} }
HorizontalScrollDecorator {} HorizontalScrollDecorator {}
} }

View file

@ -26,27 +26,26 @@ import "../.."
* Page to indicate that the application is connecting to a server. * Page to indicate that the application is connecting to a server.
*/ */
Page { Page {
property string serverName property string serverName
property string serverAddress property string serverAddress
property Page firstPage property Page firstPage
allowedOrientations: Orientation.All allowedOrientations: Orientation.All
BusyLabel { BusyLabel {
text: qsTr("Connecting to %1").arg(serverName) text: qsTr("Connecting to %1").arg(serverName)
running: true running: true
} }
onStatusChanged: { onStatusChanged: {
if (status == PageStatus.Active) { if (status == PageStatus.Active) {
console.log("Connecting page active"); apiClient.setupConnection();
ApiClient.setupConnection(); }
} }
}
Connections {
Connections { target: apiClient
target: ApiClient
onConnectionSuccess: { onConnectionSuccess: {
console.log("Login success: " + loginMessage); console.log("Login success: " + loginMessage);
pageStack.replace(Qt.resolvedUrl("LoginDialog.qml"), {"loginMessage": loginMessage, "firstPage": firstPage}); pageStack.replace(Qt.resolvedUrl("LoginDialog.qml"), {"loginMessage": loginMessage, "firstPage": firstPage});

View file

@ -28,40 +28,40 @@ import "../../"
* This dialog allows manual address entry or use one of the addresses discovered via UDP broadcasts. * This dialog allows manual address entry or use one of the addresses discovered via UDP broadcasts.
*/ */
Dialog { Dialog {
id: dialogRoot id: dialogRoot
allowedOrientations: Orientation.All allowedOrientations: Orientation.All
// Picks the address of the ComboBox if selected, otherwise the manual address entry // Picks the address of the ComboBox if selected, otherwise the manual address entry
readonly property string address: serverSelect.currentItem._address readonly property string address: serverSelect.currentItem._address
readonly property bool addressCorrect: serverSelect.currentIndex > 0 || manualAddress.acceptableInput readonly property bool addressCorrect: serverSelect.currentIndex > 0 || manualAddress.acceptableInput
readonly property string serverName: serverSelect.currentItem._name readonly property string serverName: serverSelect.currentItem._name
readonly property bool _isSetupPage: true readonly property bool _isSetupPage: true
acceptDestination: AddServerConnectingPage {
id: connectingPage
serverName: dialogRoot.serverName
serverAddress: address
firstPage: dialogRoot
}
Column {
width: parent.width
DialogHeader {
acceptText: qsTr("Connect")
title: qsTr("Connect to Jellyfin")
}
J.ServerDiscoveryModel {
id: serverModel
}
ComboBox {
id: serverSelect
label: qsTr("Server")
description: qsTr("Sailfin will try to search for Jellyfin servers on your local network automatically")
menu: ContextMenu {
acceptDestination: AddServerConnectingPage {
id: connectingPage
serverName: dialogRoot.serverName
serverAddress: address
firstPage: dialogRoot
}
Column {
width: parent.width
DialogHeader {
acceptText: qsTr("Connect")
title: qsTr("Connect to Jellyfin")
}
J.ServerDiscoveryModel {
id: serverModel
}
ComboBox {
id: serverSelect
label: qsTr("Server")
description: qsTr("Sailfin will try to search for Jellyfin servers on your local network automatically")
menu: ContextMenu {
MenuItem { MenuItem {
// Special values are cool, aren't they? // Special values are cool, aren't they?
readonly property string _address: manualAddress.text readonly property string _address: manualAddress.text
@ -106,8 +106,8 @@ Dialog {
function tryConnect() { function tryConnect() {
console.log("Hi there!") console.log("Hi there!")
ApiClient.baseUrl = address; apiClient.baseUrl = address;
//ApiClient.setupConnection() //apiClient.setupConnection()
//fakeTimer.start() //fakeTimer.start()
} }

View file

@ -46,14 +46,14 @@ Dialog {
} }
onStatusChanged: { onStatusChanged: {
if(status == PageStatus.Active) { if(status == PageStatus.Active) {
ApiClient.authenticate(username.text, password.text, true) apiClient.authenticate(username.text, password.text, true)
} }
} }
Connections { Connections {
target: ApiClient target: apiClient
onAuthenticatedChanged: { onAuthenticatedChanged: {
if (ApiClient.authenticated) { if (apiClient.authenticated) {
console.log("authenticated!") console.log("authenticated!")
pageStack.replaceAbove(null, Qt.resolvedUrl("../MainPage.qml")) pageStack.replaceAbove(null, Qt.resolvedUrl("../MainPage.qml"))
} }
@ -70,7 +70,7 @@ Dialog {
/*PublicUserModel { /*PublicUserModel {
id: userModel id: userModel
apiClient: ApiClient apiClient: appWindow.apiClient
Component.onCompleted: reload(); Component.onCompleted: reload();
}*/ }*/
@ -103,7 +103,7 @@ Dialog {
model: 0 //userModel model: 0 //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.jellyfinId).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: { onHighlightedChanged: {
if (highlighted) { if (highlighted) {

View file

@ -16,5 +16,4 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
singleton Constants 1.0 Constants.qml singleton Constants 1.0 Constants.qml
singleton ApiClient 1.0 ApiClient.qml
Utils 1.0 Utils.js Utils 1.0 Utils.js

View file

@ -69,7 +69,7 @@ int main(int argc, char *argv[]) {
if (canSanbox && !cmdParser.isSet(sandboxOption)) { if (canSanbox && !cmdParser.isSet(sandboxOption)) {
qDebug() << "Restarting in sandbox mode"; qDebug() << "Restarting in sandbox mode";
QProcess::execute(QString(SANDBOX_PROGRAM), QProcess::execute(QString(SANDBOX_PROGRAM),
QStringList() << "-p" << "harbour-sailfin.desktop" << "/usr/bin/harbour-sailfin"); QStringList() << "-p" << "harbour-sailfin.desktop" << "harbour-sailfin");
return 0; return 0;
} }