diff --git a/KomplexHubContent/MainScreen.qml b/KomplexHubContent/MainScreen.qml index 5f318d4..d2ee0a3 100644 --- a/KomplexHubContent/MainScreen.qml +++ b/KomplexHubContent/MainScreen.qml @@ -10,6 +10,7 @@ import QtQuick.Layouts import KomplexHub import KomplexHub.Controls import KomplexHub.Kero +import KomplexHubContent Rectangle { property MenuButton currentMenuButton @@ -78,6 +79,7 @@ Rectangle { currentMenuButton = this searchContainer.preferredHeight = 50 + // searchContainer.searchTerm = (pageLoader.page as ImageSearchPage).query } } @@ -99,6 +101,7 @@ Rectangle { currentMenuButton = this searchContainer.preferredHeight = 50 + // searchContainer.searchTerm = pageLoader.page.query } } @@ -176,6 +179,10 @@ Rectangle { icon: "qrc:/images/icons/icons8-search.svg" + onSearchTermChanged: () => { + pageLoader.setSearchTerm() + } + Behavior on preferredHeight { NumberAnimation { duration: Constants.normalAnimationDuration } } @@ -212,34 +219,72 @@ Rectangle { onFinished: () => { if(pageLoader.loading && pageLoader.opacity === 0) + { pageLoader.loadPage() + } else if(pageLoader.loading && pageLoader.opacity === 1) + { pageLoader.loading = false + pageLoader.updateSearchTerm(); + } } } - function fadeOutPage() { + function fadeOutPage() + { pageLoaderAnimation.from = 1 pageLoaderAnimation.to = 0 pageLoaderAnimation.start() } - function fadeInPage() { + function fadeInPage() + { if(pageLoader.sourceComponent.status != Component.Ready) + { return + } pageLoaderAnimation.from = 0 pageLoaderAnimation.to = 1 pageLoaderAnimation.start() } - function loadPage() { + function loadPage() + { sourceComponent = Qt.createComponent(page) - if(pageLoader.sourceComponent.status == Component.Ready) + if(sourceComponent.status == Component.Ready) + { fadeInPage() + } else + { statusChanged.connect(fadeInPage) + } + } + + function updateSearchTerm() + { + if(isSearchPage()) + { + searchContainer.searchTerm = pageLoader.item.query + } + } + + function setSearchTerm() + { + if(isSearchPage()) + { + pageLoader.item.query = searchContainer.searchTerm + } + } + + function isSearchPage() + { + return ( + pageLoader.item instanceof ImageSearchPage || + pageLoader.item instanceof LiveSearchPage + ); } } } diff --git a/KomplexHubContent/pages/HomePage.qml b/KomplexHubContent/pages/HomePage.qml index 5f9d151..c5b4374 100644 --- a/KomplexHubContent/pages/HomePage.qml +++ b/KomplexHubContent/pages/HomePage.qml @@ -48,7 +48,8 @@ Item { Text { color: palette.text - font.pixelSize: Constants.largeFont.pixelSize + font.pixelSize: Constants.h3Font.pixelSize + font.bold: true text: "Newest Wallpaper Packs" } @@ -57,6 +58,7 @@ Item { Layout.fillWidth: true Layout.preferredHeight: 256 Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: Constants.largeMargin Repeater { model: newestPacksModel @@ -104,7 +106,8 @@ Item { Text { color: palette.text - font.pixelSize: Constants.largeFont.pixelSize + font.pixelSize: Constants.h3Font.pixelSize + font.bold: true text: "Featured Images" } @@ -113,6 +116,7 @@ Item { Layout.fillWidth: true Layout.preferredHeight: 256 Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: Constants.largeMargin Repeater { model: featuredImagesModel @@ -159,7 +163,8 @@ Item { Text { color: palette.text - font.pixelSize: Constants.largeFont.pixelSize + font.pixelSize: Constants.h3Font.pixelSize + font.bold: true text: "Featured Videos" } @@ -168,6 +173,7 @@ Item { Layout.fillWidth: true Layout.preferredHeight: 256 Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: Constants.largeMargin Repeater { model: featuredVideosModel diff --git a/KomplexHubContent/pages/ImageSearchPage.qml b/KomplexHubContent/pages/ImageSearchPage.qml index b5ac138..b63c83f 100644 --- a/KomplexHubContent/pages/ImageSearchPage.qml +++ b/KomplexHubContent/pages/ImageSearchPage.qml @@ -1,16 +1,109 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts + import KomplexHub import KomplexHub.Controls import KomplexHub.Kero +import KomplexHubPlugin + +Item +{ + property string query + readonly property bool searchable: true + property int resultsPerRow: (resultsView.width - (64 + Constants.largeMargin)) / (256 + Constants.largeMargin) + property int rows: (resultsView.height - (Constants.largeMargin * 2) / (256 + Constants.largeMargin)) + property int resultsPerPage: resultsPerRow * rows -Item { id: imageSearchPageRoot - SearchResultList { - id: searchResultList + ImageSearchModel + { + id: searchModel + query: imageSearchPageRoot.query + resultsPerPage: imageSearchPageRoot.resultsPerPage + } + + Rectangle + { anchors.fill: parent color: palette.base + + ScrollView + { + id: resultsView + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: buttonBoxLayout.top + + GridLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.top: parent.top + + anchors.margins: Constants.largeMargin + columns: imageSearchPageRoot.resultsPerRow + + Repeater { + model: searchModel + + delegate: Item { + width: 256 + height: 256 + required property string author + required property string description + required property string uuid + required property string thumbnail + required property string authorId + + SearchResultItem + { + anchors.fill: parent + title: parent.author + author: qsTr("Pexels Images") + description: parent.description + thumbnail: parent.thumbnail + uuid: parent.uuid + } + } + } + } + } + + RowLayout { + id: buttonBoxLayout + height: 50 + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + SquareButton { + Layout.fillHeight: true + Layout.preferredWidth: 128 + + icon.source: "qrc:/images/icons/icons8-back.svg" + icon.height: 16 + icon.width: 16 + text: qsTr("Previous") + enabled: searchModel.hasPreviousPage + } + + Item { Layout.fillWidth: true } + + SquareButton { + Layout.fillHeight: true + Layout.preferredWidth: 128 + + icon.source: "qrc:/images/icons/icons8-next.svg" + icon.height: 16 + icon.width: 16 + text: qsTr("Next") + enabled: searchModel.hasNextPage + } + } } KeroErrorAvatar { @@ -37,39 +130,49 @@ Item { states: [ State { name: "loading" - when: searchResultList.state === "loading" + when: searchModel.state == ImageSearchModel.Loading - PropertyChanges { + PropertyChanges + { target: loadingOverlay opacity: 1 } - PropertyChanges { + PropertyChanges + { target: resultsView opacity: 0 } }, State { name: "empty" - when: searchResultList.state === "empty" + when: searchModel.totalResults === 0 - PropertyChanges { + PropertyChanges + { target: noResultsOverlay opacity: 1 } - PropertyChanges { + PropertyChanges + { target: resultsView opacity: 0 } + PropertyChanges { + target: buttonBoxLayout + visible: false + } }, State { name: "error" - when: searchResultList.state === "error" + when: searchModel.state == ImageSearchModel.Error - PropertyChanges { + PropertyChanges + { target: errorOverlay opacity: 1 } - PropertyChanges { + PropertyChanges + { target: resultsView opacity: 0 } diff --git a/KomplexHubPlugin/CMakeLists.txt b/KomplexHubPlugin/CMakeLists.txt index d7bdf6e..5c9632b 100644 --- a/KomplexHubPlugin/CMakeLists.txt +++ b/KomplexHubPlugin/CMakeLists.txt @@ -31,7 +31,7 @@ qt_add_qml_module( SOURCES paginators/featuredimagespaginator.h SOURCES paginators/wallpaperpaginator.h SOURCES paginators/searchpackspaginator.h - SOURCES paginators/searchimagespaginator.h + SOURCES paginators/imagesearchpaginator.h SOURCES paginators/newestshaderspaginator.h SOURCES paginators/newestcubemapspaginator.h SOURCES paginators/featuredshaderspaginator.h @@ -50,6 +50,8 @@ qt_add_qml_module( SOURCES paginators/imagepaginator.h SOURCES common/videocache.h SOURCES paginators/videopaginator.h + SOURCES imagesearchmodel.cpp + SOURCES imagesearchmodel.h ) diff --git a/KomplexHubPlugin/imagesearchmodel.cpp b/KomplexHubPlugin/imagesearchmodel.cpp new file mode 100644 index 0000000..3ab781c --- /dev/null +++ b/KomplexHubPlugin/imagesearchmodel.cpp @@ -0,0 +1,300 @@ +#include "imagesearchmodel.h" + +ImageSearchModel::ImageSearchModel(QObject *parent) : QAbstractListModel{parent} +{ + setState(Loading); + + m_paginator = new ImageSearchPaginator(this); + PaginationNotifier *notifier = static_cast(m_paginator); + + QObject::connect( + notifier, + &PaginationNotifier::resultsPerPageChanged, + this, + &ImageSearchModel::resultsPerPageChanged + ); + + QObject::connect( + notifier, + &PaginationNotifier::totalResultsChanged, + this, + &ImageSearchModel::totalResultsChanged + ); + + QObject::connect( + notifier, + &PaginationNotifier::totalPagesChanged, + this, + &ImageSearchModel::totalPagesChanged + ); + + QObject::connect( + notifier, + &PaginationNotifier::pageChanged, + this, + &ImageSearchModel::pageChanged + ); + + QObject::connect( + notifier, + &PaginationNotifier::dataChanged, + this, + &ImageSearchModel::resetDataModel + ); + + QObject::connect( + m_paginator, + &ImageSearchPaginator::queryChanged, + this, + &ImageSearchModel::queryChanged + ); +} + +auto ImageSearchModel::rowCount(const QModelIndex &) const -> int +{ + if(m_paginator != nullptr) + { + return m_paginator->count(); + } + + return 0; +} + +auto ImageSearchModel::data(const QModelIndex &index, int role) const -> QVariant +{ + if(index.row() < 0 || m_paginator == nullptr) + { + return {}; + } + + ImageCache dataPoint = m_data.at(index.row()); + + QVariant data; + + switch(static_cast(role)) + { + case UuidRole: + data = dataPoint.id; + break; + case AuthorRole: + data = dataPoint.photographer; + break; + case AuthorIdRole: + data = dataPoint.photographerId; + break; + case AuthorUrlRole: + data = dataPoint.photographerUrl; + break; + case DescriptionRole: + data = dataPoint.altText; + break; + case ThumbnailRole: + data = dataPoint.sources.value(QString::fromUtf8("tiny")); + break; + case PortraitUrlRole: + data = dataPoint.sources.value(QString::fromUtf8("portrait")); + break; + case LandscapeUrlRole: + data = dataPoint.sources.value(QString::fromUtf8("landscape")); + break; + case SmallUrlRole: + data = dataPoint.sources.value(QString::fromUtf8("small")); + break; + case OriginalUrlRole: + data = dataPoint.sources.value(QString::fromUtf8("original")); + break; + case MediumUrlRole: + data = dataPoint.sources.value(QString::fromUtf8("medium")); + break; + case LargeUrlRole: + data = dataPoint.sources.value(QString::fromUtf8("large")); + break; + case ExtraLargeUrlRole: + data = dataPoint.sources.value(QString::fromUtf8("large2x")); + break; + } + + return data; +} + +auto ImageSearchModel::index(int row, int column, const QModelIndex &parent) const -> QModelIndex +{ + Q_UNUSED(parent) + + if(row < 0 || row >= m_data.count()) + return {}; + + return createIndex(row, column, &m_data[row]); +} + +auto ImageSearchModel::columnCount(const QModelIndex &) const -> int +{ + return 0; +} + +auto ImageSearchModel::parent(const QModelIndex &) const -> QModelIndex +{ + return {}; +} + +auto ImageSearchModel::setData(const QModelIndex &, const QVariant &, int) -> bool +{ + return false; +} + +auto ImageSearchModel::state() const -> ImageSearchModel::State +{ + return m_state; +} + +auto ImageSearchModel::setState(State state) -> void +{ + if (m_state == state) + { + return; + } + + m_state = state; + emit stateChanged(); +} + +auto ImageSearchModel::resetState() -> void +{ + setState(Idle); +} + +auto ImageSearchModel::resetDataModel() -> void +{ + setState(Loading); + + //invalidate previous model data + beginRemoveRows(QModelIndex(), 0, m_data.count()); + m_data.clear(); + endRemoveRows(); + + //signal new data + beginInsertRows(QModelIndex(), 0, m_paginator->count() - 1); + auto dataset = m_paginator->data(); + + for(const ImageCache& data : std::as_const(dataset)) + { + m_data.append(data); + } + + endInsertRows(); + + setState(Idle); +} + +auto ImageSearchModel::hasNextPage() -> bool +{ + return m_paginator->page() < m_paginator->totalPages(); +} + +auto ImageSearchModel::hasPreviousPage() -> bool +{ + return m_paginator->page() > 0; +} + +auto ImageSearchModel::roleNames() const -> QHash +{ + return m_dataRoles; +} + +auto ImageSearchModel::errorString() const -> QString +{ + return m_errorString; +} + +auto ImageSearchModel::setErrorString(const QString &errorString) -> void +{ + if (m_errorString == errorString) + { + return; + } + + m_errorString = errorString; + emit errorStringChanged(); +} + +auto ImageSearchModel::resultsPerPage() const -> qsizetype +{ + if(m_paginator != nullptr) + { + return m_paginator->resultsPerPage(); + } + + return 0; +} + +auto ImageSearchModel::setResultsPerPage(qsizetype resultsPerPage) -> void +{ + if(m_paginator != nullptr) + { + m_paginator->setResultsPerPage(resultsPerPage); + } +} + +auto ImageSearchModel::totalResults() const -> qsizetype +{ + if(m_paginator != nullptr) + { + return m_paginator->totalResults(); + } + + return 0; +} + +auto ImageSearchModel::nextPage() const -> void +{ + if(m_paginator != nullptr) + { + m_paginator->next(); + } +} + +auto ImageSearchModel::previousPage() const -> void +{ + if(m_paginator != nullptr) + { + m_paginator->previous(); + } +} + +auto ImageSearchModel::query() const -> QString +{ + if(m_paginator != nullptr) + { + return m_paginator->query(); + } + + return {}; +} + +auto ImageSearchModel::setQuery(const QString &query) -> void +{ + if(m_paginator != nullptr) + { + m_paginator->setQuery(query); + } +} + +qsizetype ImageSearchModel::page() const +{ + if(m_paginator != nullptr) + { + return m_paginator->page(); + } + + return 0; +} + +auto ImageSearchModel::totalPages() const -> qsizetype +{ + if(m_paginator != nullptr) + { + return m_paginator->totalPages(); + } + + return 0; +} diff --git a/KomplexHubPlugin/imagesearchmodel.h b/KomplexHubPlugin/imagesearchmodel.h new file mode 100644 index 0000000..9977cb9 --- /dev/null +++ b/KomplexHubPlugin/imagesearchmodel.h @@ -0,0 +1,335 @@ +/* + * Komplex Wallpaper Engine + * Copyright (C) 2026 @DigitalArtifex + * https://digitalartifex.dev - https://github.com/DigitalArtifex + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ +#ifndef ImageSearchModel_H +#define ImageSearchModel_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "paginators/imagesearchpaginator.h" + +/** + * @brief The ImageSearchModel class + */ +class KOMPLEX_EXPORT ImageSearchModel : public QAbstractListModel +{ + Q_OBJECT + QML_ELEMENT +public: + + /** + * @brief Used to control the UI state + */ + enum State + { + Idle, + Loading, + Error + }; + Q_ENUM(State) + + /** + * @brief Data roles used by the view to request data + */ + enum DataRole { + UuidRole = Qt::UserRole + 1, + AuthorRole, + AuthorIdRole, + AuthorUrlRole, + DescriptionRole, + ThumbnailRole, + PortraitUrlRole, + LandscapeUrlRole, + SmallUrlRole, + OriginalUrlRole, + MediumUrlRole, + LargeUrlRole, + ExtraLargeUrlRole + }; + Q_ENUM(DataRole) + + explicit ImageSearchModel(QObject *parent = nullptr); + + auto rowCount(const QModelIndex &parent = QModelIndex()) const -> int override; + + /** + * @brief data + * Used by the view to pull data from the model. + * Required by QAbstractListModel + * @param index + * @param role + * @return + */ + auto data( + const QModelIndex &index, + int role = Qt::DisplayRole + ) const -> QVariant override; + + /** + * @brief index + * Used by the view to create an index for the data point. + * Required by QAbstractListModel + * @param row + * @param column + * @param parent + * @return + */ + auto index( + int row, + int column, + const QModelIndex &parent = QModelIndex() + ) const -> QModelIndex override; + + /** + * @brief columnCount + * Required by QAbstractListModel, but just returns 0 + * since we dont use cols + * @param parent + * @return + */ + auto columnCount( + const QModelIndex &parent = QModelIndex() + ) const -> int override; + + /** + * @brief parent + * @param index + * @return + */ + auto parent(const QModelIndex &index) const -> QModelIndex override; + + /** + * @brief setData + * Required by QAbstractListModel but not used + * @param index + * @param value + * @param role + * @return true always + */ + auto setData( + const QModelIndex &index, + const QVariant &value, + int role = Qt::EditRole + ) -> bool override; + + /** + * @brief state + * Current Model State + * @return + */ + auto state() const -> State; + + /** + * @brief roleNames + * Used to tell the view what data roles are available + * and their QML friendly names + * @return + */ + auto roleNames() const -> QHash override; + + /** + * @brief errorString + * User-friendly error string to be reported to the user + * @return + */ + auto errorString() const -> QString; + + /** + * @brief resultsPerPage + * @return + */ + auto resultsPerPage() const -> qsizetype; + + /** + * @brief setResultsPerPage + * @param resultsPerPage + */ + auto setResultsPerPage(qsizetype resultsPerPage) -> void; + + /** + * @brief totalResults + * Total number of results available + * @return + */ + auto totalResults() const -> qsizetype; + + /** + * @brief page + * @return + */ + auto page() const -> qsizetype; + + /** + * @brief totalPages + * Total pages based on the current resultsPerPage + * @return + */ + auto totalPages() const -> qsizetype; + + /** + * @brief nextPage + * Function for the QML frontend + */ + Q_INVOKABLE auto nextPage() const -> void; + + /** + * @brief previousPage + * Function for the QML frontend + */ + Q_INVOKABLE auto previousPage() const -> void; + + /** + * @brief query + * Current search query + * @return + */ + auto query() const -> QString; + + /** + * @brief setQuery + * Sets the current + * @param query + */ + auto setQuery(const QString &query) -> void; + +protected: + /** + * @brief setErrorString + * @param errorString + */ + auto setErrorString(const QString &errorString) -> void; + + /** + * @brief setState + * @param state + */ + auto setState(State state) -> void; + + /** + * @brief resetState + */ + auto resetState() -> void; + + auto resetDataModel() -> void; + + auto hasNextPage() -> bool; + + auto hasPreviousPage() -> bool; + +signals: + auto stateChanged() -> void; + auto errorStringChanged() -> void; + auto resultsPerPageChanged() -> void; + + auto pageChanged() -> void; + auto totalResultsChanged() -> void; + auto totalPagesChanged() -> void; + auto queryChanged() -> void; + +private: + static inline const QHash m_dataRoles = + { + { + static_cast(UuidRole), + QByteArray("uuid") + }, + { + static_cast(AuthorRole), + QByteArray("author") + }, + { + static_cast(AuthorIdRole), + QByteArray("authorId") + }, + { + static_cast(AuthorUrlRole), + QByteArray("authorUrl") + }, + { + static_cast(DescriptionRole), + QByteArray("description") + }, + { + static_cast(ThumbnailRole), + QByteArray("thumbnail") + }, + { + static_cast(PortraitUrlRole), + QByteArray("portrait") + }, + { + static_cast(LandscapeUrlRole), + QByteArray("landscape") + }, + { + static_cast(SmallUrlRole), + QByteArray("small") + }, + { + static_cast(OriginalUrlRole), + QByteArray("original") + }, + { + static_cast(MediumUrlRole), + QByteArray("medium") + }, + { + static_cast(LargeUrlRole), + QByteArray("large") + }, + { + static_cast(ExtraLargeUrlRole), + QByteArray("extraLarge") + } + }; + State m_state = Idle; + + QString m_errorString = QString(); + + ImageSearchPaginator *m_paginator = nullptr; + + mutable QList m_data; + + Q_PROPERTY(State state READ state WRITE setState RESET resetState NOTIFY stateChanged FINAL) + Q_PROPERTY(QString errorString READ errorString WRITE setErrorString NOTIFY errorStringChanged FINAL) + Q_PROPERTY(qsizetype resultsPerPage READ resultsPerPage WRITE setResultsPerPage NOTIFY resultsPerPageChanged FINAL) + Q_PROPERTY(qsizetype totalResults READ totalResults NOTIFY totalResultsChanged FINAL) + Q_PROPERTY(qsizetype totalPages READ totalPages NOTIFY totalPagesChanged FINAL) + Q_PROPERTY(qsizetype page READ page NOTIFY pageChanged FINAL) + Q_PROPERTY(bool hasNextPage READ hasNextPage NOTIFY pageChanged FINAL) + Q_PROPERTY(bool hasPreviousPage READ hasPreviousPage NOTIFY pageChanged FINAL) + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged FINAL) +}; +Q_DECLARE_METATYPE(ImageSearchModel) + +#endif // ImageSearchModel_H