Added ImageSearch model and QML page

This commit is contained in:
Digital Artifex
2026-06-18 01:11:16 -04:00
parent 25a51257c1
commit bf1903ddcd
6 changed files with 811 additions and 20 deletions
+49 -4
View File
@@ -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
);
}
}
}
+9 -3
View File
@@ -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
+115 -12
View File
@@ -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
}
+3 -1
View File
@@ -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
)
+300
View File
@@ -0,0 +1,300 @@
#include "imagesearchmodel.h"
ImageSearchModel::ImageSearchModel(QObject *parent) : QAbstractListModel{parent}
{
setState(Loading);
m_paginator = new ImageSearchPaginator(this);
PaginationNotifier *notifier = static_cast<PaginationNotifier*>(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<DataRole>(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<int, QByteArray>
{
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;
}
+335
View File
@@ -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 <https://www.gnu.org/licenses/>
*/
#ifndef ImageSearchModel_H
#define ImageSearchModel_H
#include <QObject>
#include <QQmlEngine>
#include <QList>
#include <QStandardPaths>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonParseError>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QAbstractListModel>
#include <QProcess>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QEventLoop>
#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<int, QByteArray> 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<int, QByteArray> m_dataRoles =
{
{
static_cast<int>(UuidRole),
QByteArray("uuid")
},
{
static_cast<int>(AuthorRole),
QByteArray("author")
},
{
static_cast<int>(AuthorIdRole),
QByteArray("authorId")
},
{
static_cast<int>(AuthorUrlRole),
QByteArray("authorUrl")
},
{
static_cast<int>(DescriptionRole),
QByteArray("description")
},
{
static_cast<int>(ThumbnailRole),
QByteArray("thumbnail")
},
{
static_cast<int>(PortraitUrlRole),
QByteArray("portrait")
},
{
static_cast<int>(LandscapeUrlRole),
QByteArray("landscape")
},
{
static_cast<int>(SmallUrlRole),
QByteArray("small")
},
{
static_cast<int>(OriginalUrlRole),
QByteArray("original")
},
{
static_cast<int>(MediumUrlRole),
QByteArray("medium")
},
{
static_cast<int>(LargeUrlRole),
QByteArray("large")
},
{
static_cast<int>(ExtraLargeUrlRole),
QByteArray("extraLarge")
}
};
State m_state = Idle;
QString m_errorString = QString();
ImageSearchPaginator *m_paginator = nullptr;
mutable QList<ImageCache> 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