mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-11-22 09:15:18 +00:00
WIP: implement search
This commit is contained in:
parent
79d378c9ed
commit
eac8faf173
|
@ -27,7 +27,7 @@ set(jellyfin-qt_HEADERS
|
||||||
include/JellyfinQt/serverdiscoverymodel.h)
|
include/JellyfinQt/serverdiscoverymodel.h)
|
||||||
|
|
||||||
add_definitions(-DSAILFIN_VERSION=\"${SAILFIN_VERSION}\")
|
add_definitions(-DSAILFIN_VERSION=\"${SAILFIN_VERSION}\")
|
||||||
add_library(jellyfin-qt ${jellyfin-qt_SOURCES} ${jellyfin-qt_HEADERS})
|
add_library(jellyfin-qt STATIC ${jellyfin-qt_SOURCES} ${jellyfin-qt_HEADERS})
|
||||||
target_include_directories(jellyfin-qt
|
target_include_directories(jellyfin-qt
|
||||||
PUBLIC "include"
|
PUBLIC "include"
|
||||||
)
|
)
|
||||||
|
|
|
@ -143,6 +143,7 @@ public:
|
||||||
Q_PROPERTY(QList<QString> includeItemTypes MEMBER m_includeItemTypes NOTIFY includeItemTypesChanged)
|
Q_PROPERTY(QList<QString> includeItemTypes MEMBER m_includeItemTypes NOTIFY includeItemTypesChanged)
|
||||||
Q_PROPERTY(bool recursive MEMBER m_recursive)
|
Q_PROPERTY(bool recursive MEMBER m_recursive)
|
||||||
Q_PROPERTY(SortOrder sortOrder MEMBER m_sortOrder NOTIFY sortOrderChanged)
|
Q_PROPERTY(SortOrder sortOrder MEMBER m_sortOrder NOTIFY sortOrderChanged)
|
||||||
|
Q_PROPERTY(QString searchTerm MEMBER m_searchTerm WRITE setSearchTerm NOTIFY searchTermChanged)
|
||||||
|
|
||||||
// Path properties
|
// Path properties
|
||||||
Q_PROPERTY(QString show MEMBER m_show NOTIFY showChanged)
|
Q_PROPERTY(QString show MEMBER m_show NOTIFY showChanged)
|
||||||
|
@ -157,6 +158,12 @@ public:
|
||||||
bool canFetchMore(const QModelIndex &parent) const override;
|
bool canFetchMore(const QModelIndex &parent) const override;
|
||||||
void fetchMore(const QModelIndex &parent) override;
|
void fetchMore(const QModelIndex &parent) override;
|
||||||
|
|
||||||
|
virtual void setSearchTerm(QString newSearchTerm) {
|
||||||
|
this->m_searchTerm = newSearchTerm;
|
||||||
|
emit searchTermChanged(newSearchTerm);
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
ModelStatus status() const { return m_status; }
|
ModelStatus status() const { return m_status; }
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
|
@ -184,6 +191,7 @@ signals:
|
||||||
void fieldsChanged(QList<QString> newFields);
|
void fieldsChanged(QList<QString> newFields);
|
||||||
void imageTypesChanged(QList<QString> newImageTypes);
|
void imageTypesChanged(QList<QString> newImageTypes);
|
||||||
void includeItemTypesChanged(const QList<QString> &newIncludeItemTypes);
|
void includeItemTypesChanged(const QList<QString> &newIncludeItemTypes);
|
||||||
|
void searchTermChanged(QString newSearchTerm);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
/**
|
/**
|
||||||
|
@ -193,6 +201,7 @@ public slots:
|
||||||
protected:
|
protected:
|
||||||
|
|
||||||
enum LoadType {
|
enum LoadType {
|
||||||
|
INITIAL_LOAD,
|
||||||
RELOAD,
|
RELOAD,
|
||||||
LOAD_MORE
|
LOAD_MORE
|
||||||
};
|
};
|
||||||
|
@ -207,6 +216,14 @@ protected:
|
||||||
* query types specific for a certain model to be available.
|
* query types specific for a certain model to be available.
|
||||||
*/
|
*/
|
||||||
virtual void addQueryParameters(QUrlQuery &query);
|
virtual void addQueryParameters(QUrlQuery &query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief setArray gets called when response data that replaces the current data has been received.
|
||||||
|
* The default implementation just resets the model, subclasses may do more advanced operations.
|
||||||
|
* @param newData The new data to store.
|
||||||
|
*/
|
||||||
|
virtual void setArray(const QJsonArray &newData);
|
||||||
|
|
||||||
ApiClient *m_apiClient = nullptr;
|
ApiClient *m_apiClient = nullptr;
|
||||||
ModelStatus m_status = Uninitialised;
|
ModelStatus m_status = Uninitialised;
|
||||||
|
|
||||||
|
@ -225,6 +242,7 @@ protected:
|
||||||
|
|
||||||
// Query properties
|
// Query properties
|
||||||
bool m_addUserId = false;
|
bool m_addUserId = false;
|
||||||
|
bool m_recursive = false;
|
||||||
QString m_parentId;
|
QString m_parentId;
|
||||||
QString m_seasonId;
|
QString m_seasonId;
|
||||||
QList<QString> m_fields = {};
|
QList<QString> m_fields = {};
|
||||||
|
@ -232,7 +250,7 @@ protected:
|
||||||
QList<QString> m_sortBy = {};
|
QList<QString> m_sortBy = {};
|
||||||
QList<QString> m_includeItemTypes = {};
|
QList<QString> m_includeItemTypes = {};
|
||||||
SortOrder m_sortOrder = Unspecified;
|
SortOrder m_sortOrder = Unspecified;
|
||||||
bool m_recursive = false;
|
QString m_searchTerm;
|
||||||
|
|
||||||
QHash<int, QByteArray> m_roles;
|
QHash<int, QByteArray> m_roles;
|
||||||
|
|
||||||
|
@ -245,7 +263,7 @@ private:
|
||||||
/**
|
/**
|
||||||
* @brief Generates roleNames based on the first record in m_array.
|
* @brief Generates roleNames based on the first record in m_array.
|
||||||
*/
|
*/
|
||||||
void generateFields();
|
void generateFields(const QJsonArray &newData);
|
||||||
QString sortByToString(SortOptions::SortBy sortBy);
|
QString sortByToString(SortOptions::SortBy sortBy);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -270,6 +288,31 @@ public slots:
|
||||||
void onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData);
|
void onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Lists search items and provides a nice animation when search items change their position.
|
||||||
|
*/
|
||||||
|
class SearchModel : public ItemModel {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit SearchModel(QObject *parent = nullptr);
|
||||||
|
Q_PROPERTY(int searchTimeout MEMBER m_searchTimeout NOTIFY searchTimeoutChanged)
|
||||||
|
signals:
|
||||||
|
void searchTimeoutChanged(int newTimeout);
|
||||||
|
protected:
|
||||||
|
void setArray(const QJsonArray &newData) override;
|
||||||
|
void setSearchTerm(QString searchTerm) override;
|
||||||
|
/**
|
||||||
|
* @brief DIFF_SIZE amount of items to keep track of when the position changes.
|
||||||
|
*/
|
||||||
|
const static size_t DIFF_SIZE = 10;
|
||||||
|
// Store the ids of the
|
||||||
|
std::array<QString, DIFF_SIZE> m_diffTracker;
|
||||||
|
|
||||||
|
// Time in ms before we can search again
|
||||||
|
int m_searchTimeout = 500;
|
||||||
|
QTimer m_searchTimeoutTimer;
|
||||||
|
};
|
||||||
|
|
||||||
class UserViewModel : public ApiModel {
|
class UserViewModel : public ApiModel {
|
||||||
public:
|
public:
|
||||||
explicit UserViewModel (QObject *parent = nullptr);
|
explicit UserViewModel (QObject *parent = nullptr);
|
||||||
|
|
|
@ -138,7 +138,7 @@ public slots:
|
||||||
*
|
*
|
||||||
* The default implementation makes a GET request to getDataUrl() and parses the resulting JSON,
|
* The default implementation makes a GET request to getDataUrl() and parses the resulting JSON,
|
||||||
* which should be enough for most cases. Consider overriding getDataUrl() and
|
* which should be enough for most cases. Consider overriding getDataUrl() and
|
||||||
* canRelaod() if possible. Manual overrides need to make sure that
|
* canReload() if possible. Manual overrides need to make sure that
|
||||||
* they're calling setStatus(Status), setError(QNetworkReply::NetworkError) and
|
* they're calling setStatus(Status), setError(QNetworkReply::NetworkError) and
|
||||||
* setErrorString() to let the QML side know what this thing is up to.
|
* setErrorString() to let the QML side know what this thing is up to.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -30,11 +30,12 @@ ApiModel::ApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject
|
||||||
void ApiModel::reload() {
|
void ApiModel::reload() {
|
||||||
this->setStatus(Loading);
|
this->setStatus(Loading);
|
||||||
m_startIndex = 0;
|
m_startIndex = 0;
|
||||||
load(RELOAD);
|
|
||||||
|
load(m_array.isEmpty() ? INITIAL_LOAD : RELOAD);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApiModel::load(LoadType type) {
|
void ApiModel::load(LoadType type) {
|
||||||
qDebug() << (type == RELOAD ? "RELOAD" : "LOAD_MORE");
|
qDebug() << (type == RELOAD ? "RELOAD" : type == INITIAL_LOAD ? "INITIAL_LOAD" : "LOAD_MORE");
|
||||||
if (m_apiClient == nullptr) {
|
if (m_apiClient == nullptr) {
|
||||||
qWarning() << "Please set the apiClient property before (re)loading";
|
qWarning() << "Please set the apiClient property before (re)loading";
|
||||||
return;
|
return;
|
||||||
|
@ -81,17 +82,21 @@ void ApiModel::load(LoadType type) {
|
||||||
if (m_recursive) {
|
if (m_recursive) {
|
||||||
query.addQueryItem("Recursive", "true");
|
query.addQueryItem("Recursive", "true");
|
||||||
}
|
}
|
||||||
|
if (!m_searchTerm.isEmpty()) {
|
||||||
|
query.addQueryItem("searchTerm", m_searchTerm);
|
||||||
|
}
|
||||||
addQueryParameters(query);
|
addQueryParameters(query);
|
||||||
QNetworkReply *rep = m_apiClient->get(m_path, query);
|
QNetworkReply *rep = m_apiClient->get(m_path, query);
|
||||||
connect(rep, &QNetworkReply::finished, this, [this, type, rep]() {
|
connect(rep, &QNetworkReply::finished, this, [this, type, rep]() {
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(rep->readAll());
|
QJsonDocument doc = QJsonDocument::fromJson(rep->readAll());
|
||||||
|
QJsonArray items;
|
||||||
if (!m_hasRecordResponse) {
|
if (!m_hasRecordResponse) {
|
||||||
if (!doc.isArray()) {
|
if (!doc.isArray()) {
|
||||||
qWarning() << "Object is not an array!";
|
qWarning() << "Object is not an array!";
|
||||||
this->setStatus(Error);
|
this->setStatus(Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this->m_array = doc.array();
|
items = doc.array();
|
||||||
} else {
|
} else {
|
||||||
if (!doc.isObject()) {
|
if (!doc.isObject()) {
|
||||||
qWarning() << "Object is not an object!";
|
qWarning() << "Object is not an object!";
|
||||||
|
@ -120,10 +125,10 @@ void ApiModel::load(LoadType type) {
|
||||||
this->setStatus(Error);
|
this->setStatus(Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
QJsonArray items = obj["Items"].toArray();
|
items = obj["Items"].toArray();
|
||||||
switch(type) {
|
switch(type) {
|
||||||
|
case INITIAL_LOAD:
|
||||||
case RELOAD:
|
case RELOAD:
|
||||||
this->m_array = items;
|
|
||||||
break;
|
break;
|
||||||
case LOAD_MORE:
|
case LOAD_MORE:
|
||||||
this->beginInsertRows(QModelIndex(), m_array.size(), m_array.size() + items.size() - 1);
|
this->beginInsertRows(QModelIndex(), m_array.size(), m_array.size() + items.size() - 1);
|
||||||
|
@ -138,27 +143,35 @@ void ApiModel::load(LoadType type) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (type == RELOAD) {
|
|
||||||
generateFields();
|
if (type == INITIAL_LOAD) {
|
||||||
|
generateFields(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type == INITIAL_LOAD || type == RELOAD) {
|
||||||
|
for (auto it = items.begin(); it != items.end(); it++){
|
||||||
|
JsonHelper::convertToCamelCase(*it);
|
||||||
|
}
|
||||||
|
setArray(items);
|
||||||
|
}
|
||||||
|
|
||||||
this->setStatus(Ready);
|
this->setStatus(Ready);
|
||||||
rep->deleteLater();
|
rep->deleteLater();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApiModel::generateFields() {
|
void ApiModel::generateFields(const QJsonArray &newData) {
|
||||||
if (m_array.size() == 0) return;
|
if (newData.size() == 0) return;
|
||||||
this->beginResetModel();
|
|
||||||
int i = Qt::UserRole + 1;
|
int i = Qt::UserRole + 1;
|
||||||
if (!m_array[0].isObject()) {
|
if (!newData[0].isObject()) {
|
||||||
qWarning() << "Iterator is not an object?";
|
qWarning() << "Iterator is not an object?";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Walks over the keys in the first record and adds them to the rolenames.
|
// Walks over the keys in the first record and adds them to the rolenames.
|
||||||
// This assumes the back-end has the same keys for every record. I could technically
|
// This assumes the back-end has the same keys for every record. I could technically
|
||||||
// go over all records to be really sure, but no-one got time for a O(n²) algorithm, so
|
// go over all records to be really sure, but no-one got time for a O(n) algorithm, so
|
||||||
// this heuristic hopefully suffices.
|
// this heuristic hopefully suffices.
|
||||||
QJsonObject ob = m_array[0].toObject();
|
QJsonObject ob = newData[0].toObject();
|
||||||
for (auto jt = ob.begin(); jt != ob.end(); jt++) {
|
for (auto jt = ob.begin(); jt != ob.end(); jt++) {
|
||||||
QString keyName = jt.key();
|
QString keyName = jt.key();
|
||||||
keyName[0] = keyName[0].toLower();
|
keyName[0] = keyName[0].toLower();
|
||||||
|
@ -167,9 +180,12 @@ void ApiModel::generateFields() {
|
||||||
m_roles.insert(i++, keyArr);
|
m_roles.insert(i++, keyArr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (auto it = m_array.begin(); it != m_array.end(); it++){
|
|
||||||
JsonHelper::convertToCamelCase(*it);
|
}
|
||||||
}
|
|
||||||
|
void ApiModel::setArray(const QJsonArray &newData) {
|
||||||
|
this->beginResetModel();
|
||||||
|
m_array = newData;
|
||||||
this->endResetModel();
|
this->endResetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,6 +261,124 @@ void ItemModel::onUserDataChanged(const QString &itemId, QSharedPointer<UserData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SearchModel::SearchModel(QObject *parent)
|
||||||
|
: ItemModel("/Users/{{user}}/Items", true, false, parent) {
|
||||||
|
m_diffTracker.fill("");
|
||||||
|
m_searchTimeoutTimer.setInterval(m_searchTimeout);
|
||||||
|
connect(this, &SearchModel::searchTimeoutChanged, &m_searchTimeoutTimer, &QTimer::setInterval);
|
||||||
|
m_searchTimeoutTimer.setSingleShot(true);
|
||||||
|
connect(&m_searchTimeoutTimer, &QTimer::timeout, this, &ApiModel::reload);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchModel::setArray(const QJsonArray &newData) {
|
||||||
|
// Calculate the movement of the search results in the first DIFF_SIZE results
|
||||||
|
qDebug() << "Calculating diff";
|
||||||
|
int maxLoop = std::min(static_cast<int>(DIFF_SIZE), newData.size());
|
||||||
|
|
||||||
|
// Loop over the first DIFF_SIZE entries of newData to check where they came from.
|
||||||
|
for (int i = 0; i < maxLoop; i++) {
|
||||||
|
|
||||||
|
// Loop over all the entries of our difference tracker
|
||||||
|
int j;
|
||||||
|
for (j = 0; j < static_cast<int>(m_diffTracker.size()); j++) {
|
||||||
|
// Stop when we're hitting empty entries
|
||||||
|
if (m_diffTracker[static_cast<size_t>(j)].isEmpty()) {
|
||||||
|
j = DIFF_SIZE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newData[i].toObject()["id"] == m_diffTracker[static_cast<size_t>(j)]) {
|
||||||
|
// The old entry is on the same place as the previous entry
|
||||||
|
if (i == j) break;
|
||||||
|
// Entry[j] has moved to position i
|
||||||
|
this->beginMoveRows(QModelIndex(), j, j, QModelIndex(), i);
|
||||||
|
m_array.insert(i, newData[i]);
|
||||||
|
// If the old position j is greater than the new position i, we need to remove
|
||||||
|
// j + 1, since the index has shifted due the insertion we just made.
|
||||||
|
m_array.removeAt(j > i ? j + 1 : j);
|
||||||
|
qDebug() << j << " -> " << i;
|
||||||
|
this->endMoveRows();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j == static_cast<int>(m_diffTracker.size())) {
|
||||||
|
// New item id could not be found in the old item id list.
|
||||||
|
// Remove the old items and add the new item
|
||||||
|
if (i < m_array.size()) {
|
||||||
|
this->beginRemoveRows(QModelIndex(), i, i);
|
||||||
|
m_array.removeAt(i);
|
||||||
|
this->endRemoveRows();
|
||||||
|
qDebug() << i << " replaced";
|
||||||
|
} else {
|
||||||
|
qDebug() << i << " inserted";
|
||||||
|
}
|
||||||
|
this->beginInsertRows(QModelIndex(), i, i);
|
||||||
|
m_array.insert(i, newData[i]);
|
||||||
|
this->endInsertRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove items from the point where we just left until we reach the previous size or the
|
||||||
|
// amount of items we keep the difference on. Should only be executed when the amount
|
||||||
|
// of new results is smaller than the old amount.
|
||||||
|
for (int i = maxLoop; i < static_cast<int>(DIFF_SIZE) && i < m_array.size(); i++) {
|
||||||
|
this->beginRemoveRows(QModelIndex(), i, i);
|
||||||
|
m_array.removeAt(i);
|
||||||
|
this->endRemoveRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the new positions of the items
|
||||||
|
for (int i = 0; i < maxLoop; i++) {
|
||||||
|
if (!newData[i].isObject() || !newData[i].toObject().contains("id")) continue;
|
||||||
|
m_diffTracker[static_cast<size_t>(i)] = newData[i].toObject()["id"].toString();
|
||||||
|
qDebug() << i << " changed";
|
||||||
|
}
|
||||||
|
for (size_t i = static_cast<size_t>(maxLoop); i < DIFF_SIZE; i++) {
|
||||||
|
m_diffTracker[i].clear();
|
||||||
|
qDebug() << i << " removed";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the amount of data is smaller than the amount of data of which we track their differences in position,
|
||||||
|
// we're done hear.
|
||||||
|
// if (newData.size() < static_cast<int>(DIFF_SIZE)) return;
|
||||||
|
|
||||||
|
int common = std::max(static_cast<int>(DIFF_SIZE), std::min(newData.size(), m_array.size()));
|
||||||
|
for (int i = static_cast<int>(DIFF_SIZE); i < common; i++) {
|
||||||
|
m_array.replace(i, newData[i]);
|
||||||
|
qDebug() << i << " replaced (tail)";
|
||||||
|
}
|
||||||
|
|
||||||
|
emit dataChanged(index(static_cast<int>(DIFF_SIZE)), index(common - 1));
|
||||||
|
|
||||||
|
// Handle extra or fewer items in the area that we do not diff
|
||||||
|
if (newData.size() > m_array.size()) {
|
||||||
|
this->beginInsertRows(QModelIndex(), common, newData.size() - 1);
|
||||||
|
for (int i = common; i < newData.size(); i++) {
|
||||||
|
m_array.append(newData[i]);
|
||||||
|
qDebug() << i << " appended (tail)";
|
||||||
|
}
|
||||||
|
this->endInsertRows();
|
||||||
|
} else if (newData.size() < m_array.size()) {
|
||||||
|
this->beginRemoveRows(QModelIndex(), common, m_array.size() - 1);
|
||||||
|
for (int i = common; i < m_array.size(); i++) {
|
||||||
|
qDebug() << i << " removed (tail)";
|
||||||
|
m_array.removeAt(i);
|
||||||
|
}
|
||||||
|
this->endRemoveRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchModel::setSearchTerm(QString searchTerm) {
|
||||||
|
if (m_searchTimeoutTimer.isActive()) {
|
||||||
|
m_searchTimeoutTimer.stop();
|
||||||
|
}
|
||||||
|
m_searchTimeoutTimer.start();
|
||||||
|
m_searchTerm = searchTerm;
|
||||||
|
emit searchTermChanged(searchTerm);
|
||||||
|
}
|
||||||
|
|
||||||
PublicUserModel::PublicUserModel(QObject *parent)
|
PublicUserModel::PublicUserModel(QObject *parent)
|
||||||
: ApiModel ("/users/public", false, false, parent) { }
|
: ApiModel ("/users/public", false, false, parent) { }
|
||||||
|
|
||||||
|
@ -280,5 +414,6 @@ void registerModels(const char *URI) {
|
||||||
qmlRegisterType<ShowNextUpModel>(URI, 1, 0, "ShowNextUpModel");
|
qmlRegisterType<ShowNextUpModel>(URI, 1, 0, "ShowNextUpModel");
|
||||||
qmlRegisterType<ShowSeasonsModel>(URI, 1, 0, "ShowSeasonsModel");
|
qmlRegisterType<ShowSeasonsModel>(URI, 1, 0, "ShowSeasonsModel");
|
||||||
qmlRegisterType<ShowEpisodesModel>(URI, 1, 0, "ShowEpisodesModel");
|
qmlRegisterType<ShowEpisodesModel>(URI, 1, 0, "ShowEpisodesModel");
|
||||||
|
qmlRegisterType<SearchModel>(URI, 1, 0, "SearchModel");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ set(sailfin_QML_SOURCES
|
||||||
qml/components/music/NarrowAlbumCover.qml
|
qml/components/music/NarrowAlbumCover.qml
|
||||||
qml/components/music/WideAlbumCover.qml
|
qml/components/music/WideAlbumCover.qml
|
||||||
qml/components/music/SongDelegate.qml
|
qml/components/music/SongDelegate.qml
|
||||||
|
qml/components/search/SearchResults.qml
|
||||||
qml/components/videoplayer/VideoError.qml
|
qml/components/videoplayer/VideoError.qml
|
||||||
qml/components/videoplayer/VideoHud.qml
|
qml/components/videoplayer/VideoHud.qml
|
||||||
qml/components/GlassyBackground.qml
|
qml/components/GlassyBackground.qml
|
||||||
|
@ -62,7 +63,7 @@ set(sailfin_QML_SOURCES
|
||||||
add_executable(harbour-sailfin ${harbour-sailfin_SOURCES} ${sailfin_QML_SOURCES})
|
add_executable(harbour-sailfin ${harbour-sailfin_SOURCES} ${sailfin_QML_SOURCES})
|
||||||
target_link_libraries(harbour-sailfin PRIVATE Qt5::Gui Qt5::Qml Qt5::Quick SailfishApp::SailfishApp
|
target_link_libraries(harbour-sailfin PRIVATE Qt5::Gui Qt5::Qml Qt5::Quick SailfishApp::SailfishApp
|
||||||
# Note: this may break when the compiler changes. -rdynamic and -pie seem to be needed for the
|
# Note: this may break when the compiler changes. -rdynamic and -pie seem to be needed for the
|
||||||
# invoker/booster to work
|
# invoker/booster to work
|
||||||
jellyfin-qt "-Wl,-rpath,${CMAKE_INSTALL_LIBDIR} -rdynamic -pie")
|
jellyfin-qt "-Wl,-rpath,${CMAKE_INSTALL_LIBDIR} -rdynamic -pie")
|
||||||
|
|
||||||
install(TARGETS harbour-sailfin
|
install(TARGETS harbour-sailfin
|
||||||
|
|
|
@ -30,6 +30,7 @@ SilicaItem {
|
||||||
property string fallbackImage
|
property string fallbackImage
|
||||||
property bool usingFallbackImage
|
property bool usingFallbackImage
|
||||||
property color fallbackColor: Theme.highlightColor
|
property color fallbackColor: Theme.highlightColor
|
||||||
|
property bool hideFallbackColor: false
|
||||||
|
|
||||||
property var __parentPage : null
|
property var __parentPage : null
|
||||||
property bool alreadyLoaded: false
|
property bool alreadyLoaded: false
|
||||||
|
@ -56,7 +57,7 @@ SilicaItem {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: root.fillMode
|
fillMode: root.fillMode
|
||||||
opacity: 1
|
opacity: hideFallbackColor ? 0.0 : 1.0
|
||||||
source: alreadyLoaded || [PageStatus.Active, PageStatus.Deactivating].indexOf(__parentPage.status) >= 0 ? root.source : ""
|
source: alreadyLoaded || [PageStatus.Active, PageStatus.Deactivating].indexOf(__parentPage.status) >= 0 ? root.source : ""
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
if (status == Image.Ready) {
|
if (status == Image.Ready) {
|
||||||
|
@ -73,6 +74,7 @@ SilicaItem {
|
||||||
GradientStop { position: 1.0; color: Theme.highlightDimmerFromColor(fallbackColor, Theme.colorScheme); }
|
GradientStop { position: 1.0; color: Theme.highlightDimmerFromColor(fallbackColor, Theme.colorScheme); }
|
||||||
}
|
}
|
||||||
opacity: 0
|
opacity: 0
|
||||||
|
visible: !hideFallbackColor
|
||||||
}
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
|
@ -132,7 +134,7 @@ SilicaItem {
|
||||||
when: realImage.status === Image.Ready
|
when: realImage.status === Image.Ready
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: realImage
|
target: realImage
|
||||||
//opacity: 1
|
opacity: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
40
sailfish/qml/components/search/SearchResults.qml
Normal file
40
sailfish/qml/components/search/SearchResults.qml
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import QtQuick 2.6
|
||||||
|
import Sailfish.Silica 1.0
|
||||||
|
|
||||||
|
import nl.netsoj.chris.Jellyfin 1.0
|
||||||
|
|
||||||
|
SilicaListView {
|
||||||
|
id: root
|
||||||
|
model: query.length ? searchModel : 0
|
||||||
|
property alias query: searchModel.searchTerm
|
||||||
|
delegate: ListItem {
|
||||||
|
width: parent.width
|
||||||
|
height: Theme.itemSizeLarge
|
||||||
|
Label {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width
|
||||||
|
text: model.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add: Transition {
|
||||||
|
NumberAnimation { property: "scale"; from: 0.0001; to: 1.0; }
|
||||||
|
FadeAnimation { from: 0.0; to: 1.0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
move: Transition {
|
||||||
|
NumberAnimation {
|
||||||
|
properties: "x,y"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove: Transition {
|
||||||
|
NumberAnimation { property: "scale"; from: 1.0; to: 0.0001; }
|
||||||
|
FadeAnimation { from: 1.0; to: 0.0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchModel {
|
||||||
|
id: searchModel
|
||||||
|
apiClient: ApiClient
|
||||||
|
recursive: true
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import Sailfish.Silica 1.0
|
||||||
import nl.netsoj.chris.Jellyfin 1.0
|
import nl.netsoj.chris.Jellyfin 1.0
|
||||||
|
|
||||||
import "../components"
|
import "../components"
|
||||||
|
import "../components/search"
|
||||||
import "../"
|
import "../"
|
||||||
import "../Utils.js" as Utils
|
import "../Utils.js" as Utils
|
||||||
|
|
||||||
|
@ -36,6 +37,7 @@ Page {
|
||||||
allowedOrientations: Orientation.All
|
allowedOrientations: Orientation.All
|
||||||
|
|
||||||
SilicaFlickable {
|
SilicaFlickable {
|
||||||
|
id: mainView
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
// PullDownMenu and PushUpMenu must be declared in SilicaFlickable, SilicaListView or SilicaGridView
|
// PullDownMenu and PushUpMenu must be declared in SilicaFlickable, SilicaListView or SilicaGridView
|
||||||
|
@ -66,73 +68,92 @@ Page {
|
||||||
id: mediaLibraryModel2
|
id: mediaLibraryModel2
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
}
|
}
|
||||||
|
Item {
|
||||||
|
id: searchBoxPlaceholder
|
||||||
|
width: parent.width
|
||||||
|
height: searchBox.height
|
||||||
|
|
||||||
MoreSection {
|
SearchField {
|
||||||
text: qsTr("Resume watching")
|
id: searchBox
|
||||||
clickable: false
|
|
||||||
busy: userResumeModel.status == ApiModel.Loading
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
sourceComponent: carrouselView
|
placeholderText: qsTr("Search")
|
||||||
property alias itemModel: userResumeModel
|
|
||||||
property string collectionType: "series"
|
|
||||||
|
|
||||||
UserItemResumeModel {
|
|
||||||
id: userResumeModel
|
|
||||||
apiClient: ApiClient
|
|
||||||
limit: 12
|
|
||||||
recursive: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MoreSection {
|
|
||||||
text: qsTr("Next up")
|
|
||||||
clickable: false
|
|
||||||
busy: showNextUpModel.status == ApiModel.Loading
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: carrouselView
|
|
||||||
property alias itemModel: showNextUpModel
|
|
||||||
property string collectionType: "series"
|
|
||||||
|
|
||||||
ShowNextUpModel {
|
|
||||||
id: showNextUpModel
|
|
||||||
apiClient: ApiClient
|
|
||||||
limit: 12
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UserViewModel {
|
Label {
|
||||||
id: mediaLibraryModel
|
text: "%1 results".arg(searchView.count)
|
||||||
apiClient: ApiClient
|
|
||||||
}
|
}
|
||||||
Repeater {
|
|
||||||
model: mediaLibraryModel
|
Column {
|
||||||
|
id: homeView
|
||||||
|
width: parent.width
|
||||||
MoreSection {
|
MoreSection {
|
||||||
text: model.name
|
text: qsTr("Resume watching")
|
||||||
busy: userItemModel.status != ApiModel.Ready
|
clickable: false
|
||||||
|
busy: userResumeModel.status == ApiModel.Loading
|
||||||
onHeaderClicked: pageStack.push(Qt.resolvedUrl("itemdetails/CollectionPage.qml"), {"itemId": model.id})
|
|
||||||
Loader {
|
Loader {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
sourceComponent: carrouselView
|
sourceComponent: carrouselView
|
||||||
property alias itemModel: userItemModel
|
property alias itemModel: userResumeModel
|
||||||
property string collectionType: model.collectionType || ""
|
property string collectionType: "series"
|
||||||
|
|
||||||
UserItemLatestModel {
|
UserItemResumeModel {
|
||||||
id: userItemModel
|
id: userResumeModel
|
||||||
|
apiClient: ApiClient
|
||||||
|
limit: 12
|
||||||
|
recursive: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MoreSection {
|
||||||
|
text: qsTr("Next up")
|
||||||
|
clickable: false
|
||||||
|
busy: showNextUpModel.status == ApiModel.Loading
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: carrouselView
|
||||||
|
property alias itemModel: showNextUpModel
|
||||||
|
property string collectionType: "series"
|
||||||
|
|
||||||
|
ShowNextUpModel {
|
||||||
|
id: showNextUpModel
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
parentId: model.id
|
|
||||||
limit: 12
|
limit: 12
|
||||||
}
|
}
|
||||||
Connections {
|
}
|
||||||
target: mediaLibraryModel
|
}
|
||||||
onStatusChanged: {
|
|
||||||
console.log("MediaLibraryModel status " + status)
|
UserViewModel {
|
||||||
if (status == ApiModel.Ready) {
|
id: mediaLibraryModel
|
||||||
userItemModel.reload()
|
apiClient: ApiClient
|
||||||
|
}
|
||||||
|
Repeater {
|
||||||
|
model: mediaLibraryModel
|
||||||
|
MoreSection {
|
||||||
|
text: model.name
|
||||||
|
busy: userItemModel.status != ApiModel.Ready
|
||||||
|
|
||||||
|
onHeaderClicked: pageStack.push(Qt.resolvedUrl("itemdetails/CollectionPage.qml"), {"itemId": model.id})
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: carrouselView
|
||||||
|
property alias itemModel: userItemModel
|
||||||
|
property string collectionType: model.collectionType || ""
|
||||||
|
|
||||||
|
UserItemLatestModel {
|
||||||
|
id: userItemModel
|
||||||
|
apiClient: ApiClient
|
||||||
|
parentId: model.id
|
||||||
|
limit: 12
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: mediaLibraryModel
|
||||||
|
onStatusChanged: {
|
||||||
|
console.log("MediaLibraryModel status " + status)
|
||||||
|
if (status == ApiModel.Ready) {
|
||||||
|
userItemModel.reload()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,6 +188,42 @@ Page {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SearchResults {
|
||||||
|
id: searchView
|
||||||
|
anchors.fill: parent
|
||||||
|
header: Item {
|
||||||
|
width: parent.width
|
||||||
|
height: searchBox.height
|
||||||
|
}
|
||||||
|
query: searchBox.text
|
||||||
|
visible: false
|
||||||
|
opacity: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
states: [
|
||||||
|
State {
|
||||||
|
name: "search"
|
||||||
|
when: searchBox.text.length
|
||||||
|
PropertyChanges {
|
||||||
|
target: searchView
|
||||||
|
visible: true
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: mainView
|
||||||
|
opacity: 0
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
ParentChange {
|
||||||
|
target: searchBox
|
||||||
|
parent: searchView.headerItem
|
||||||
|
}
|
||||||
|
StateChangeScript {
|
||||||
|
script: searchBox.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
if (status == PageStatus.Active) {
|
if (status == PageStatus.Active) {
|
||||||
appWindow.itemData = null
|
appWindow.itemData = null
|
||||||
|
@ -181,7 +238,7 @@ Page {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads models if not laoded. Set force to true to reload models
|
* Loads models if not loaded. Set force to true to reload models
|
||||||
* even if loaded.
|
* even if loaded.
|
||||||
*/
|
*/
|
||||||
function loadModels(force) {
|
function loadModels(force) {
|
||||||
|
|
|
@ -68,6 +68,7 @@ Page {
|
||||||
top: parent.top
|
top: parent.top
|
||||||
bottom: parent.bottom
|
bottom: parent.bottom
|
||||||
}
|
}
|
||||||
|
hideFallbackColor: true
|
||||||
source: ApiClient.baseUrl + "/Users/" + ApiClient.userId + "/Images/Primary?tag=" + loggedInUser.primaryImageTag
|
source: ApiClient.baseUrl + "/Users/" + ApiClient.userId + "/Images/Primary?tag=" + loggedInUser.primaryImageTag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue