diff --git a/CMakeLists.txt b/CMakeLists.txt index e8101a9..ceec6f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,13 +20,12 @@ set(QML_IMPORT_PATH ${QT_QML_OUTPUT_DIRECTORY} FORCE ) -find_package(Qt6 6.8 REQUIRED COMPONENTS Core Gui Widgets Qml Quick QuickControls2 QuickTimeline ShaderTools) +find_package(Qt6 6.8 REQUIRED COMPONENTS Core Gui Widgets Qml Quick QuickControls2 QuickTimeline ShaderTools Concurrent) qt_standard_project_setup() qt_add_executable(${CMAKE_PROJECT_NAME} README.md LICENSE - images/icons/icons8-video.svg ) qt_add_resources(${CMAKE_PROJECT_NAME} "configuration" PREFIX "/" @@ -59,6 +58,7 @@ qt_add_resources(${CMAKE_PROJECT_NAME} "app_images" "images/icons/icons8-user-account.svg" "images/icons/icons8-view-more.svg" "images/icons/icons8-video.svg" + "images/icons/pexels-icon-filled-256.svg" "images/kero/kero_build_1.png" "images/kero/kero_build_2.png" "images/kero/kero_build_3.png" diff --git a/KomplexHubContent/CMakeLists.txt b/KomplexHubContent/CMakeLists.txt index b1aea49..ede62b4 100644 --- a/KomplexHubContent/CMakeLists.txt +++ b/KomplexHubContent/CMakeLists.txt @@ -18,6 +18,7 @@ qt6_add_qml_module(KomplexHubContent "pages/UserProfilePage.qml" "pages/WallpaperSettingsPage.qml" QML_FILES pages/VideoSearchPage.qml + QML_FILES views/ImageView.qml ) diff --git a/KomplexHubContent/MainScreen.qml b/KomplexHubContent/MainScreen.qml index 6c5c595..a2a931a 100644 --- a/KomplexHubContent/MainScreen.qml +++ b/KomplexHubContent/MainScreen.qml @@ -317,21 +317,19 @@ Rectangle { } } - KeroBuildingAnimation { + KeroBuildingAnimation + { id: buildingOverlay anchors.fill: parent visible: false } - KeroLoadingAnimation { + KeroLoadingAnimation + { id: loadingAnimation anchors.fill: parent - // OpacityAnimator on opacity { - // duration: 150 - // } - visible: opacity > 0 opacity: 0 } diff --git a/KomplexHubContent/pages/HomePage.qml b/KomplexHubContent/pages/HomePage.qml index c2ac82c..2f45953 100644 --- a/KomplexHubContent/pages/HomePage.qml +++ b/KomplexHubContent/pages/HomePage.qml @@ -1,4 +1,4 @@ -import QtQuick +import QtQuick 2.15 import QtQuick.Controls import QtQuick.Layouts import QtQuick.Effects @@ -20,18 +20,54 @@ Item { { id: newestPacksModel resultsPerPage: homePageRoot.resultsPerRow + + onStateChanged: () => + { + if(state == NewestPacksModel.Loading) + { + console.log("Newest packs loading") + } + else if(state == NewestPacksModel.Idle) + { + console.log("Newest packs finished") + } + } } FeaturedImagesModel { id: featuredImagesModel resultsPerPage: homePageRoot.resultsPerRow + + onStateChanged: () => + { + if(state == FeaturedImagesModel.Loading) + { + console.log("Featured Images loading") + } + else if(state == FeaturedImagesModel.Idle) + { + console.log("Featured Images finished") + } + } } FeaturedVideosModel { id: featuredVideosModel resultsPerPage: homePageRoot.resultsPerRow + + onStateChanged: () => + { + if(state == FeaturedVideosModel.Loading) + { + console.log("Featured Videos loading") + } + else if(state == FeaturedVideosModel.Idle) + { + console.log("Featured Videos finished") + } + } } Rectangle @@ -210,10 +246,11 @@ Item { { anchors.fill: parent title: parent.author - author: qsTr("Pexels Images") description: parent.description thumbnail: parent.thumbnail uuid: parent.uuid + + pexels: true } } } @@ -312,10 +349,9 @@ Item { { anchors.fill: parent title: parent.author - author: parent.author - description: "Video provided by Pexels" thumbnail: parent.thumbnail uuid: parent.uuid + pexels: true } } } @@ -357,6 +393,13 @@ Item { } } + Component.onCompleted: () => + { + featuredImagesModel.nextPage() + featuredVideosModel.nextPage() + newestPacksModel.nextPage() + } + KeroLoadingAnimation { id: loadingAnimation diff --git a/KomplexHubContent/pages/ImageSearchPage.qml b/KomplexHubContent/pages/ImageSearchPage.qml index 995b378..31aa640 100644 --- a/KomplexHubContent/pages/ImageSearchPage.qml +++ b/KomplexHubContent/pages/ImageSearchPage.qml @@ -9,217 +9,43 @@ import KomplexHubPlugin Item { - property string query - readonly property bool searchable: true - property int resultsPerRow: (resultsContainer.width - (Constants.largeMargin * 2)) / (resultWidth + Constants.mediumMargin) - property int rows: ((resultsContainer.height - (Constants.largeMargin * 2)) / (resultHeight + Constants.mediumMargin)) - property int resultsPerPage: resultsPerRow * rows - property int emptyWidth: (resultsContainer.width - (Constants.largeMargin * 2)) - (resultsPerRow * (resultWidth + Constants.mediumMargin)) - property int resultWidth: 256 - property int resultHeight: 245 + id: rootItem - id: imageSearchPageRoot + property string query + readonly property bool searchable: paginator.searchable ImageSearchModel { id: searchModel - query: imageSearchPageRoot.query - resultsPerPage: imageSearchPageRoot.resultsPerPage + query: rootItem.query + resultsPerPage: paginator.resultsPerPage } - Rectangle + PaginatorGrid { + id: paginator anchors.fill: parent - color: palette.base + model: searchModel + loading: model.state === ImageSearchModel.Loading - Rectangle + delegate: Item { - id: resultsContainer - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: buttonBoxLayout.top + width: 256 + height: 245 + required property string author + required property string description + required property string uuid + required property string thumbnail + required property string authorId - color: "transparent" - - ScrollView + SearchResultItem { - id: resultsView anchors.fill: parent - - RowLayout - { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.top: parent.top - anchors.margins: Constants.largeMargin - - Item - { - Layout.fillHeight: true - Layout.preferredWidth: emptyWidth / 2 - Layout.maximumWidth: emptyWidth / 2 - } - - GridLayout - { - Layout.fillHeight: true - Layout.fillWidth: true - - columns: imageSearchPageRoot.resultsPerRow - columnSpacing: Constants.mediumMargin - rowSpacing: Constants.mediumMargin - - Repeater - { - model: searchModel - - delegate: Item - { - width: 256 - height: 245 - 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: 35 - - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: Constants.largeMargin - - 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 - - onTriggered: () => searchModel.previousPage() - } - - 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 - - onTriggered: () => searchModel.nextPage() + title: parent.author + description: parent.description + thumbnail: parent.thumbnail + uuid: parent.uuid } } } - - KeroErrorAvatar - { - id: errorOverlay - - anchors.fill: parent - opacity: 0 - } - - KeroNoResultsAvatar - { - id: noResultsOverlay - - anchors.fill: parent - opacity: 0 - } - - KeroLoadingAnimation - { - id: loadingOverlay - - anchors.fill: parent - opacity: 0 - } - - states: [ - State - { - name: "loading" - when: searchModel.state == ImageSearchModel.Loading - - PropertyChanges - { - target: loadingOverlay - opacity: 1 - } - PropertyChanges - { - target: resultsView - opacity: 0 - } - }, - State - { - name: "empty" - when: searchModel.totalResults === 0 - - PropertyChanges - { - target: noResultsOverlay - opacity: 1 - } - PropertyChanges - { - target: resultsView - opacity: 0 - } - PropertyChanges - { - target: buttonBoxLayout - visible: false - } - }, - State - { - name: "error" - when: searchModel.state == ImageSearchModel.Error - - PropertyChanges - { - target: errorOverlay - opacity: 1 - } - PropertyChanges - { - target: resultsView - opacity: 0 - } - } - ] } diff --git a/KomplexHubContent/pages/LiveSearchPage.qml b/KomplexHubContent/pages/LiveSearchPage.qml index b5ac138..8b08a14 100644 --- a/KomplexHubContent/pages/LiveSearchPage.qml +++ b/KomplexHubContent/pages/LiveSearchPage.qml @@ -3,76 +3,47 @@ import QtQuick.Controls import KomplexHub import KomplexHub.Controls import KomplexHub.Kero +import KomplexHubPlugin -Item { - id: imageSearchPageRoot +Item +{ + id: rootItem - SearchResultList { - id: searchResultList - anchors.fill: parent - color: palette.base + property string query + readonly property bool searchable: paginator.searchable + + VideoSearchModel + { + id: searchModel + query: rootItem.query + resultsPerPage: paginator.resultsPerPage } - KeroErrorAvatar { - id: errorOverlay - + PaginatorGrid + { + id: paginator anchors.fill: parent - opacity: 0 - } + model: searchModel + loading: model.state === VideoSearchModel.Loading - KeroNoResultsAvatar { - id: noResultsOverlay + delegate: Item + { + width: 256 + height: 245 + required property string author + required property string description + required property string uuid + required property string thumbnail + required property string authorId - anchors.fill: parent - opacity: 0 - } - - KeroLoadingAnimation { - id: loadingOverlay - - anchors.fill: parent - opacity: 0 - } - - states: [ - State { - name: "loading" - when: searchResultList.state === "loading" - - PropertyChanges { - target: loadingOverlay - opacity: 1 - } - PropertyChanges { - target: resultsView - opacity: 0 - } - }, - State { - name: "empty" - when: searchResultList.state === "empty" - - PropertyChanges { - target: noResultsOverlay - opacity: 1 - } - PropertyChanges { - target: resultsView - opacity: 0 - } - }, - State { - name: "error" - when: searchResultList.state === "error" - - PropertyChanges { - target: errorOverlay - opacity: 1 - } - PropertyChanges { - target: resultsView - opacity: 0 + SearchResultItem + { + anchors.fill: parent + title: parent.author + description: parent.description + thumbnail: parent.thumbnail + uuid: parent.uuid } } - ] + } } diff --git a/KomplexHubContent/pages/VideoSearchPage.qml b/KomplexHubContent/pages/VideoSearchPage.qml index 04cfa07..56683b0 100644 --- a/KomplexHubContent/pages/VideoSearchPage.qml +++ b/KomplexHubContent/pages/VideoSearchPage.qml @@ -9,217 +9,43 @@ import KomplexHubPlugin Item { - property string query - readonly property bool searchable: true - property int resultsPerRow: (resultsContainer.width - (Constants.largeMargin * 2)) / (resultWidth + Constants.mediumMargin) - property int rows: ((resultsContainer.height - (Constants.largeMargin * 2)) / (resultHeight + Constants.mediumMargin)) - property int resultsPerPage: resultsPerRow * rows - property int emptyWidth: (resultsContainer.width - (Constants.largeMargin * 2)) - (resultsPerRow * (resultWidth + Constants.mediumMargin)) - property int resultWidth: 256 - property int resultHeight: 245 + id: rootItem - id: videoSearchPageRoot + property string query + readonly property bool searchable: paginator.searchable VideoSearchModel { id: searchModel - query: videoSearchPageRoot.query - resultsPerPage: videoSearchPageRoot.resultsPerPage + query: rootItem.query + resultsPerPage: paginator.resultsPerPage } - Rectangle + PaginatorGrid { + id: paginator anchors.fill: parent - color: palette.base + model: searchModel + loading: model.state === VideoSearchModel.Loading - Rectangle + delegate: Item { - id: resultsContainer - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: buttonBoxLayout.top + width: 256 + height: 245 + required property string author + required property string description + required property string uuid + required property string thumbnail + required property string authorId - color: "transparent" - - ScrollView + SearchResultItem { - id: resultsView anchors.fill: parent - - RowLayout - { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.top: parent.top - anchors.margins: Constants.largeMargin - - Item - { - Layout.fillHeight: true - Layout.preferredWidth: emptyWidth / 2 - Layout.maximumWidth: emptyWidth / 2 - } - - GridLayout - { - Layout.fillHeight: true - Layout.fillWidth: true - - columns: videoSearchPageRoot.resultsPerRow - columnSpacing: Constants.mediumMargin - rowSpacing: Constants.mediumMargin - - Repeater - { - model: searchModel - - delegate: Item { - width: resultWidth - height: resultHeight - - required property string uuid - required property string author - required property string authorId - required property string authorUrl - required property string thumbnail - - SearchResultItem - { - anchors.fill: parent - title: parent.author - author: parent.author - description: "Video provided by Pexels" - thumbnail: parent.thumbnail - uuid: parent.uuid - } - } - } - } - } - } - } - - RowLayout - { - id: buttonBoxLayout - height: 35 - - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: Constants.largeMargin - - 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 - - onTriggered: () => searchModel.previousPage() - } - - 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 - - onTriggered: () => searchModel.nextPage() + title: parent.author + description: parent.description + thumbnail: parent.thumbnail + uuid: parent.uuid } } } - - KeroErrorAvatar - { - id: errorOverlay - - anchors.fill: parent - opacity: 0 - } - - KeroNoResultsAvatar - { - id: noResultsOverlay - - anchors.fill: parent - opacity: 0 - } - - KeroLoadingAnimation - { - id: loadingOverlay - - anchors.fill: parent - opacity: 0 - } - - states: [ - State - { - name: "loading" - when: searchModel.state == VideoSearchModel.Loading - - PropertyChanges - { - target: loadingOverlay - opacity: 1 - } - PropertyChanges - { - target: resultsView - opacity: 0 - } - }, - State - { - name: "empty" - when: searchModel.totalResults === 0 - - PropertyChanges - { - target: noResultsOverlay - opacity: 1 - } - PropertyChanges - { - target: resultsView - opacity: 0 - } - PropertyChanges - { - target: buttonBoxLayout - visible: false - } - }, - State - { - name: "error" - when: searchModel.state == VideoSearchModel.Error - - PropertyChanges - { - target: errorOverlay - opacity: 1 - } - PropertyChanges - { - target: resultsView - opacity: 0 - } - } - ] } diff --git a/KomplexHubContent/views/ImageView.qml b/KomplexHubContent/views/ImageView.qml new file mode 100644 index 0000000..95415bf --- /dev/null +++ b/KomplexHubContent/views/ImageView.qml @@ -0,0 +1,98 @@ +import QtQuick 2.15 +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Effects + +import KomplexHub +import KomplexHub.Controls +import KomplexHub.Kero +import KomplexHubPlugin + +Item +{ + property string uuid + property string author + property string authorUrl + property string description + property string thumbnail + property string portrait + property string landscape + property string small + property string original + property string medium + property string large + property string extraLarge + property string portraitSize + property string landscapeSize + property string smallSize + property string originalSize + property string mediumSize + property string largeSize + property string extraLargeSize + + property int imageHeight + property int imageWidth + + id: rootItem + + Rectangle + { + anchors.fill: parent + color: palette.base + + ColumnLayout + { + anchors.fill: parent + anchors.margins: Constants.largeMargin + spacing: Constants.largeMargin + + RowLayout + { + Layout.fillWidth: true + + /** Spacer **/ + Item { Layout.fillWidth: true } + + Image + { + width: imageWidth + height: imageHeight + transform: Image.PreserveAspectFit + source: "qrc:/images/icons/pexels-icon-filled-256.svg" + } + + /** Spacer **/ + Item { Layout.fillWidth: true } + } + + Text + { + id: titleText + text: rootItem.author + + font.bold: true + font.pixelSize: Constants.h3Font.pixelSize + + Layout.fillWidth: true + } + + Text + { + id: descriptionText + text: rootItem.description + + font.pointSize: Constants.h4Font.pixelSize + } + + Image + { + width: 64 + height: 64 + transform: Image.PreserveAspectFit + source: "qrc:/images/icons/pexels-icon-filled-256.svg" + + Layout.alignment: Qt.AlignBottom | Qt.AlignRight + } + } + } +} diff --git a/KomplexHubModule/Controls/CMakeLists.txt b/KomplexHubModule/Controls/CMakeLists.txt index 951c286..ae31ee7 100644 --- a/KomplexHubModule/Controls/CMakeLists.txt +++ b/KomplexHubModule/Controls/CMakeLists.txt @@ -14,5 +14,7 @@ qt6_add_qml_module(KomplexHubModule_Controls "Throbber.qml" "SearchResultItem.qml" "SearchResultList.qml" + QML_FILES PaginationBox.qml + QML_FILES PaginatorGrid.qml ) diff --git a/KomplexHubModule/Controls/PaginationBox.qml b/KomplexHubModule/Controls/PaginationBox.qml new file mode 100644 index 0000000..6d38a1e --- /dev/null +++ b/KomplexHubModule/Controls/PaginationBox.qml @@ -0,0 +1,55 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +import KomplexHub + +Item +{ + id: itemRoot + + property int page: 0 + property int totalPages: 0 + + property bool enablePrevious: false + property bool enableNext: false + + signal next + signal previous + + RowLayout + { + id: buttonBoxLayout + anchors.fill: parent + + 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: itemRoot.enablePrevious + + onTriggered: () => itemRoot.previous() + } + + 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: itemRoot.enableNext + + onTriggered: () => itemRoot.next() + } + } +} diff --git a/KomplexHubModule/Controls/PaginatorGrid.qml b/KomplexHubModule/Controls/PaginatorGrid.qml new file mode 100644 index 0000000..ab00155 --- /dev/null +++ b/KomplexHubModule/Controls/PaginatorGrid.qml @@ -0,0 +1,384 @@ +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: (resultsContainer.width - (Constants.largeMargin * 2)) / (resultWidth + Constants.mediumMargin) + property int rows: ((resultsContainer.height - (Constants.largeMargin * 2)) / (resultHeight + Constants.mediumMargin)) + property int resultsPerPage: resultsPerRow * rows + property int emptyWidth: (resultsContainer.width - (Constants.largeMargin * 2)) - (resultsPerRow * (resultWidth + Constants.mediumMargin)) + property int resultWidth: 256 + property int resultHeight: 245 + property bool loading: false + + property var model + + property Component delegate + + id: rootItem + + KeroErrorAvatar + { + id: errorOverlay + + anchors.fill: parent + opacity: 0 + } + + KeroNoResultsAvatar + { + id: noResultsOverlay + + anchors.fill: parent + opacity: 0 + } + + KeroChoiceAvatar + { + id: noSearchOverlay + + title: qsTr("No Search Term") + description: qsTr("Kero needs a subject or term to search for wallpapers") + + anchors.fill: parent + opacity: 0 + } + + KeroLoadingAnimation + { + id: loadingOverlay + + anchors.fill: parent + opacity: 0 + } + + Rectangle + { + id: resultsContainer + anchors.fill: parent + color: palette.base + + RowLayout + { + anchors.fill: parent + anchors.margins: Constants.largeMargin + + Item + { + Layout.fillHeight: true + Layout.preferredWidth: emptyWidth / 2 + Layout.maximumWidth: emptyWidth / 2 + } + + GridView + { + id: resultsView + Layout.fillHeight: true + Layout.fillWidth: true + + cellWidth: 256 + Constants.mediumMargin + cellHeight: 245 + Constants.mediumMargin + focus: true + model: rootItem.model + + delegate: rootItem.delegate + + add: Transition + { + NumberAnimation + { + properties: "x" + from: resultsContainer.width + duration: 250 + } + NumberAnimation + { + properties: "opacity,scale" + to: 1.0 + duration: 500 + } + } + + move: Transition + { + NumberAnimation + { + properties: "x,y" + duration: 250 + } + } + + populate: Transition + { + NumberAnimation + { + properties: "opacity,scale" + to: 1.0 + duration: 500 + } + } + + remove: Transition + { + NumberAnimation + { + properties: "x" + to: 0 + duration: 250 + } + NumberAnimation + { + properties: "opacity" + to: 0 + duration: 250 + } + } + } + } + + PaginationBox + { + height: 35 + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Constants.largeMargin + + onNext: () => model.nextPage() + onPrevious: () => model.previousPage() + + enablePrevious: model.hasPreviousPage + enableNext: model.hasNextPage + } + } + + states: [ + State + { + name: "loading" + when: loading + + PropertyChanges + { + target: loadingOverlay + opacity: 1 + } + PropertyChanges + { + target: resultsContainer + opacity: 0 + } + PropertyChanges + { + target: noSearchOverlay + opacity: 0 + } + PropertyChanges + { + target: errorOverlay + opacity: 0 + } + PropertyChanges + { + target: buttonBoxLayout + visible: false + } + }, + State + { + name: "empty" + when: model.totalResults === 0 && model.query.length > 0 + + PropertyChanges + { + target: noResultsOverlay + opacity: 1 + } + PropertyChanges + { + target: noSearchOverlay + opacity: 0 + } + PropertyChanges + { + target: resultsContainer + opacity: 0 + } + PropertyChanges + { + target: buttonBoxLayout + visible: false + } + }, + State + { + name: "nosearch" + when: model.query.length === 0 + + PropertyChanges + { + target: noResultsOverlay + opacity: 0 + } + PropertyChanges + { + target: noSearchOverlay + opacity: 1 + } + PropertyChanges + { + target: resultsContainer + opacity: 0 + } + PropertyChanges + { + target: buttonBoxLayout + visible: false + } + }, + State + { + name: "idle" + when: model.totalResults !== 0 && !loading + + PropertyChanges + { + target: loadingOverlay + opacity: 0.0 + } + PropertyChanges + { + target: resultsContainer + opacity: 1.0 + } + PropertyChanges + { + target: buttonBoxLayout + visible: true + } + }, + State + { + name: "error" + when: model.errorString.length > 0 + + PropertyChanges + { + target: errorOverlay + opacity: 1.0 + } + PropertyChanges + { + target: resultsContainer + opacity: 0.0 + } + PropertyChanges + { + target: noSearchOverlay + opacity: 0.0 + } + PropertyChanges + { + target: noResultsOverlay + opacity: 0.0 + } + } + ] + + transitions: [ + Transition + { + from: "empty" + to: "loading" + NumberAnimation + { + target: noResultsOverlay + duration: 250 + property: "opacity"; + } + NumberAnimation + { + target: loadingOverlay + duration: 250 + property: "opacity"; + } + }, + Transition + { + from: "nosearch" + to: "loading" + NumberAnimation + { + target: noSearchOverlay + duration: 250 + property: "opacity"; + } + NumberAnimation + { + target: loadingOverlay + duration: 250 + property: "opacity"; + } + }, + Transition + { + from: "loading" + to: "nosearch" + NumberAnimation + { + target: noSearchOverlay + duration: 250 + property: "opacity"; + } + NumberAnimation + { + target: loadingOverlay + duration: 250 + property: "opacity"; + } + }, + Transition + { + from: "loading" + to: "empty" + NumberAnimation + { + target: noResultsOverlay + duration: 250 + property: "opacity"; + } + NumberAnimation + { + target: loadingOverlay + duration: 250 + property: "opacity"; + } + }, + Transition + { + from: "loading" + to: "idle" + NumberAnimation + { + target: loadingOverlay + duration: 250 + property: "opacity"; + } + NumberAnimation + { + target: resultsView + duration: 250 + property: "opacity"; + } + } + ] +} diff --git a/KomplexHubModule/Controls/SearchResultItem.qml b/KomplexHubModule/Controls/SearchResultItem.qml index e26cf7e..4238bd2 100644 --- a/KomplexHubModule/Controls/SearchResultItem.qml +++ b/KomplexHubModule/Controls/SearchResultItem.qml @@ -2,13 +2,16 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import KomplexHub + Item { - property string author: "Author" - property string description: "This is a description of the search result item" - property string title: "Wallpaper Title" + property string author: "" + property string description: "" + property string title: "" property string thumbnail: "" property string uuid: "" property bool selected: false + property bool pexels: false signal triggered @@ -48,6 +51,22 @@ Item { id: thumnailImage source: searchResultItemRoot.thumbnail } + + Image + { + antialiasing: true + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Constants.mediumMargin + + width: 24 + height: 24 + transform: Image.PreserveAspectFit + opacity: 0.5 + source: "qrc:/images/icons/pexels-icon-filled-256.svg" + + visible: searchResultItemRoot.pexels + } } Text { @@ -62,6 +81,8 @@ Item { Layout.alignment: Qt.AlignHCenter | Qt.AlignTop } + Item { Layout.fillHeight: true } + Text { color: palette.text font.pixelSize: 12 @@ -70,6 +91,7 @@ Item { Layout.alignment: Qt.AlignLeft | Qt.AlignBottom Layout.preferredWidth: 250 + visible: !searchResultItemRoot.pexels } Text { @@ -88,6 +110,8 @@ Item { Layout.maximumWidth: 236 Layout.bottomMargin: 3 } + + Item { Layout.fillHeight: true } } Behavior on color { diff --git a/KomplexHubPlugin/CMakeLists.txt b/KomplexHubPlugin/CMakeLists.txt index b4894d1..2002866 100644 --- a/KomplexHubPlugin/CMakeLists.txt +++ b/KomplexHubPlugin/CMakeLists.txt @@ -19,9 +19,7 @@ qt_add_qml_module( newestpacksmodel.cpp common/komplex_global.h common/slidingcachecontroller.h - common/paginator.h - common/coreservices.h common/coreservices.cpp common/logging.h @@ -30,7 +28,7 @@ qt_add_qml_module( SOURCES paginators/featuredpackspaginator.h SOURCES paginators/featuredimagespaginator.h SOURCES paginators/wallpaperpaginator.h - SOURCES paginators/searchpackspaginator.h + SOURCES paginators/packsearchpaginator.h SOURCES paginators/imagesearchpaginator.h SOURCES paginators/newestshaderspaginator.h SOURCES paginators/newestcubemapspaginator.h @@ -54,6 +52,10 @@ qt_add_qml_module( SOURCES imagesearchmodel.h SOURCES videosearchmodel.h SOURCES videosearchmodel.cpp + SOURCES + SOURCES + SOURCES packsearchmodel.cpp + SOURCES packsearchmodel.h ) diff --git a/KomplexHubPlugin/common/imagecache.h b/KomplexHubPlugin/common/imagecache.h index 65da3ea..95b9fc4 100644 --- a/KomplexHubPlugin/common/imagecache.h +++ b/KomplexHubPlugin/common/imagecache.h @@ -18,6 +18,7 @@ struct KOMPLEX_EXPORT ImageCache QString photographerUrl; qint64 photographerId; QMap sources; + QMap sourceSizes; QString url; auto operator == (const ImageCache &other) const -> bool @@ -33,6 +34,7 @@ struct KOMPLEX_EXPORT ImageCache other.photographerUrl == photographerUrl && other.photographerId == photographerId && other.sources == sources && + other.sourceSizes == sourceSizes && other.url == url ); } diff --git a/KomplexHubPlugin/common/komplex_global.h b/KomplexHubPlugin/common/komplex_global.h index 520cab7..a0f1a57 100644 --- a/KomplexHubPlugin/common/komplex_global.h +++ b/KomplexHubPlugin/common/komplex_global.h @@ -18,6 +18,8 @@ */ #ifndef KOMPLEX_GLOBAL_H #define KOMPLEX_GLOBAL_H +#include +#include #ifdef KOMPLEX_PLUGIN #define KOMPLEX_EXPORT Q_DECL_EXPORT @@ -61,4 +63,7 @@ #define KOMPLEX_ENDPOINT_PACKS_ITEM "packs/item" #define KOMPLEX_ENDPOINT_PACKS_NEWEST "packs/newest" +inline static qsizetype nullsize = std::numeric_limits::min(); +inline static std::chrono::milliseconds KOMPLEX_RATE_LIMIT(500); + #endif // KOMPLEX_GLOBAL_H diff --git a/KomplexHubPlugin/common/paginator.h b/KomplexHubPlugin/common/paginator.h index 4d20b93..b6669a0 100644 --- a/KomplexHubPlugin/common/paginator.h +++ b/KomplexHubPlugin/common/paginator.h @@ -39,12 +39,14 @@ class KOMPLEX_EXPORT PaginationNotifier : public QObject protected: explicit PaginationNotifier(QObject *parent = nullptr) : QObject(parent) {} signals: - auto dataChanged() -> void; + auto fetchComplete() -> void; + auto fetching() -> void; auto resultsPerPageChanged() -> void; auto totalResultsChanged() -> void; auto totalPagesChanged() -> void; auto pageChanged() -> void; auto countChanged() -> void; + auto queryChanged() -> void; }; /** @@ -57,7 +59,21 @@ class KOMPLEX_EXPORT Paginator : public PaginationNotifier public: explicit Paginator(QObject *parent = nullptr) : PaginationNotifier(parent) { + QObject::connect + ( + &m_cacheController, + &SlidingCacheNotifier::fetchComplete, + this, + &PaginationNotifier::fetchComplete + ); + QObject::connect + ( + &m_cacheController, + &SlidingCacheNotifier::fetching, + this, + &PaginationNotifier::fetching + ); } /** @@ -67,7 +83,15 @@ public: */ auto next() -> void { - setOffset(m_offset + m_resultsPerPage); + if(m_offset != nullsize) + { + setOffset(m_offset + m_resultsPerPage); + } + else + { + controller()->init(); + setOffset(0); + } } /** @@ -77,7 +101,15 @@ public: */ auto previous() -> void { - setOffset(m_offset - m_resultsPerPage); + if(m_offset != nullsize && m_offset > m_resultsPerPage) + { + setOffset(m_offset - m_resultsPerPage); + } + else + { + controller()->init(); + setOffset(0); + } } /** @@ -95,8 +127,6 @@ public: */ auto setResultsPerPage(qsizetype resultsPerPage) -> void { - QMutexLocker dataLocker(&m_dataMutex); - if(resultsPerPage == m_resultsPerPage) { return; @@ -104,18 +134,53 @@ public: m_resultsPerPage = resultsPerPage; Q_EMIT static_cast(this)->resultsPerPageChanged(); + } - m_data = m_cacheController.chunk(m_offset, m_resultsPerPage); - Q_EMIT static_cast(this)->dataChanged(); + /** + * @brief query + * Current query string, if the API Endpoint supports queries + * @return + */ + [[nodiscard]] + auto query() const -> QString + { + return m_query; + } + + /** + * @brief setQuery + * Sets the new query string and updates the controller + * @param query + */ + auto setQuery(const QString &query) -> void + { + if(query == m_query) + { + return; + } + + reset(); + + m_query = query; + Q_EMIT queryChanged(); + + controller()->init(); + setOffset(0); } /** * @brief data * @return */ - auto data() const -> QList + auto data() const -> const QList { - return m_data; + if(boundaryCheck(m_resultsPerPage)) + { + return m_cacheController.chunk(m_offset, m_resultsPerPage); + } + + qsizetype count = (m_cacheController.totalCount() - m_offset); + return m_cacheController.chunk(m_offset, count); } /** @@ -124,7 +189,18 @@ public: */ auto count() const -> qsizetype { - return m_data.count(); + if(m_offset == nullsize) + { + return 0; + } + + if(boundaryCheck(m_resultsPerPage)) + { + return m_resultsPerPage; + } + + qsizetype count = (m_cacheController.totalCount() - m_offset); + return count; } /** @@ -132,16 +208,9 @@ public: * @param index * @return */ - auto at(qsizetype index) const -> T + auto at(qsizetype index) const -> const T& { - QMutexLocker dataLocker(&m_dataMutex); - - if(index < 0 || index >= m_data.count()) - { - return {}; - } - - return m_data.at(index); + return m_cacheController.at(m_offset + index); } /** @@ -181,6 +250,16 @@ public: protected: + auto queryable() const -> bool + { + return m_queryable; + } + + auto setQueryable(bool queryable) -> void + { + m_queryable = queryable; + } + /** * @brief controller * Reference to the underlying cache controller @@ -198,16 +277,11 @@ protected: */ auto setOffset(qsizetype offset) -> void { - if(offset == m_offset && offset != std::numeric_limits::infinity()) + if(offset == m_offset) { return; } - if(offset == std::numeric_limits::infinity()) - { - offset = 0; - } - m_offset = offset; if(m_offset >= totalResults()) @@ -220,26 +294,11 @@ protected: m_offset = 0; } - QList data = m_cacheController.chunk(m_offset, m_resultsPerPage); - setData(data); + m_cacheController.focus(offset, m_resultsPerPage); - Q_EMIT this->pageChanged(); - } - - /** - * @brief setData - * @param data - */ - auto setData(const QList &data) - { - m_dataMutex.lock(); - - m_data.clear(); - m_data.append(std::move(data)); - m_dataMutex.unlock(); - - Q_EMIT this->countChanged(); - Q_EMIT this->dataChanged(); + Q_EMIT totalResultsChanged(); + Q_EMIT totalPagesChanged(); + Q_EMIT pageChanged(); } /** @@ -257,26 +316,34 @@ protected: */ auto reset() -> void { - m_dataMutex.lock(); - - m_data.clear(); + m_query.clear(); + m_offset = nullsize; controller()->reset(); - m_dataMutex.unlock(); - - Q_EMIT totalPagesChanged(); - Q_EMIT dataChanged(); - Q_EMIT pageChanged(); - - setOffset(std::numeric_limits::infinity()); } private: + /** + * @brief boundaryCheck + * Checks if the index is within the bounds of the dataset + * @param index + * @return + */ + auto boundaryCheck(qsizetype index) const -> bool + { + if(index < 0 || (m_offset + index) >= m_cacheController.totalCount()) + { + return false; + } + + return true; + } + /** * @brief m_offset * Current absolute offset. Used in fetch operations */ - qsizetype m_offset = std::numeric_limits::infinity(); + mutable qsizetype m_offset = nullsize; /** * @brief m_resultsPerPage @@ -294,16 +361,22 @@ private: */ SlidingCacheController m_cacheController; - /** - * @brief m_data - * Internal data copy - */ - QList m_data; - /** * @brief m_dataMutex */ mutable QMutex m_dataMutex; + + /** + * @brief m_query + * The query string used in fetch operations + */ + QString m_query; + + /** + * @brief m_queryable + * Controls the queryable state of the paginator + */ + bool m_queryable = false; }; #endif // PAGINATOR_H diff --git a/KomplexHubPlugin/common/slidingcachecontroller.h b/KomplexHubPlugin/common/slidingcachecontroller.h index 71699f6..91846a1 100644 --- a/KomplexHubPlugin/common/slidingcachecontroller.h +++ b/KomplexHubPlugin/common/slidingcachecontroller.h @@ -23,6 +23,7 @@ #include #include #include +#include #include #include "fetchresult.h" @@ -40,6 +41,9 @@ class KOMPLEX_EXPORT SlidingCacheNotifier : public QObject Q_OBJECT signals: auto countChanged() -> void; + auto fetching() -> void; + auto fetchComplete() -> void; + auto windowSizeChanged() -> void; }; /** @@ -50,6 +54,12 @@ signals: template class KOMPLEX_EXPORT SlidingCacheController : public SlidingCacheNotifier { + /** + * @brief FetchFunction + * Convenience def used by setFetch + */ + typedef std::function (qsizetype offset, qsizetype limit)> FetchFunction; + public: explicit SlidingCacheController() : SlidingCacheNotifier{} @@ -70,9 +80,7 @@ public: * QObject * @param fetch */ - auto setFetch( - const std::function (qsizetype offset, qsizetype limit)> &fetch - ) -> void + auto setFetch(const FetchFunction &fetch) -> void { m_fetch = std::move(fetch); } @@ -85,25 +93,26 @@ public: * @return CacheObject* or nullptr */ [[nodiscard]] - auto at(qsizetype index) const -> T + auto at(qsizetype index) const -> const T& { QMutexLocker cacheLocker(&m_cacheMutex); + T defaultValue; if(!externalBoundaryCheck(index)) { - return {}; + return defaultValue; } if(!const_cast(this)->slideWindow(index)) { - return {}; + return defaultValue; } qsizetype internalIndex = this->internalIndex(index); if(!internalBoundaryCheck(index)) { - return {}; + return defaultValue; } return m_cache.at(index); @@ -144,19 +153,7 @@ public: count = m_cache.count() - index; } - QList dataChunk; - - for(qsizetype i = index; (i - index) < count; ++i) - { - T data = m_cache.at(i); - - if(data != T{}) - { - dataChunk.append(data); - } - } - - return dataChunk; + return m_cache.mid(index, count); } /** @@ -167,7 +164,7 @@ public: [[nodiscard]] auto totalCount() const -> qsizetype { - if(m_totalCount == std::numeric_limits::infinity()) + if(m_totalCount == nullsize) { return 0; } @@ -195,10 +192,53 @@ public: QMutexLocker cacheLocker(&m_cacheMutex); m_cache.clear(); - setTotalCount(std::numeric_limits::infinity()); + setTotalCount(nullsize); m_windowOffset = 0; } + auto focus(qsizetype index, qsizetype count) -> void + { + if(inFocus(index, count) || !externalBoundaryCheck(index)) + { + return; + } + + if(!externalBoundaryCheck(index + count)) + { + slideWindow(index); + } + } + + auto init() -> bool + { + if(m_totalCount != nullsize) + { + return false; + } + + return slideForward(); + } + + auto windowSize() const -> qsizetype + { + if(m_windowSize == nullsize) + { + return SLIDING_CACHE_WINDOW_SIZE; + } + + return m_windowSize; + } + + auto setWindowSize(qsizetype windowSize) -> void + { + if(windowSize == m_windowSize) + { + return; + } + + + } + protected: /** * @brief setTotalCount @@ -215,6 +255,16 @@ protected: } private: + auto inFocus(qsizetype index, qsizetype count) -> bool + { + qsizetype internalIndex = this->internalIndex(index); + + return + ( + internalBoundaryCheck(internalIndex, internalIndex + count) + ); + } + /** * @brief externalBoundaryCheck * @@ -224,12 +274,29 @@ private: * @return True if index is within the external dataset boundary * OR an initial fetch has not been performed. */ - auto externalBoundaryCheck(qsizetype index) const -> bool + template + auto externalBoundaryCheck(Ts... index) const -> bool { - return ( + static_assert + ( + (std::is_same_v && ...), + "All arguments must be qsizetype" + ); + + bool safe = false; + + ((safe = externalBoundaryCheckHelper(index)),...); + + return safe; + } + + auto externalBoundaryCheckHelper(qsizetype index) const -> bool + { + return + ( index >= 0 && ( - m_totalCount == std::numeric_limits::infinity() || + m_totalCount == nullsize || index < m_totalCount ) ); @@ -242,12 +309,18 @@ private: * @param index * @return True if index is within the internal dataset boundary */ - auto internalBoundaryCheck(qsizetype index) const -> bool + template + auto internalBoundaryCheck(Ts... index) const -> bool { - return ( - index >= 0 && - index < m_cache.count() + static_assert + ( + (std::is_same_v && ...), + "All arguments must be qsizetype" ); + + bool safe = false; + ((safe = index >= 0 && index < m_cache.count()), ...); + return safe; } /** @@ -280,14 +353,6 @@ private: } } - if(m_totalCount == std::numeric_limits::infinity()) - { - if(!slideForward()) - { - return false; - } - } - return true; } @@ -299,20 +364,41 @@ private: auto slideForward() -> bool { LOG_ERROR_X(!m_fetch, - "SlidingCacheController::slideBackward", + "SlidingCacheController::slideForward", "Fetch function not set", false ); + Q_EMIT fetching(); + qsizetype movementSize = SLIDING_CACHE_WINDOW_SIZE; + qsizetype prefetchThreshold = SLIDING_CACHE_PREFETCH_THRESHOLD; + + if(m_windowSize != nullsize) + { + movementSize = m_windowSize; + + if(prefetchThreshold >= m_windowSize) + { + prefetchThreshold = std::floor( + static_cast(movementSize) * 0.8 + ); + } + } + + LOG_ERROR_X(movementSize <= 0 || prefetchThreshold <= 0, + "SlidingCacheController::slideForward", + "Invalid movement size", + false + ); //after initial inflation we should only arrive here when the prefetch threshold is reached if(!m_cache.isEmpty()) { - movementSize -= SLIDING_CACHE_PREFETCH_THRESHOLD; + movementSize -= prefetchThreshold; } - if(m_totalCount != std::numeric_limits::infinity()) + if(m_totalCount != nullsize) { m_windowOffset += movementSize; } @@ -321,6 +407,8 @@ private: m_windowOffset = 0; } + QThread::sleep(KOMPLEX_RATE_LIMIT); + FetchResult result = m_fetch( m_windowOffset, movementSize @@ -337,6 +425,8 @@ private: if(result.data.isEmpty()) { m_windowOffset -= movementSize; + + Q_EMIT fetchComplete(); return false; } @@ -359,6 +449,8 @@ private: m_cache.append(std::move(result.data)); } + Q_EMIT fetchComplete(); + return true; } @@ -375,7 +467,31 @@ private: false ); - qsizetype movementSize = SLIDING_CACHE_WINDOW_SIZE - SLIDING_CACHE_PREFETCH_THRESHOLD; + Q_EMIT fetching(); + + qsizetype movementSize = SLIDING_CACHE_WINDOW_SIZE; + qsizetype prefetchThreshold = SLIDING_CACHE_PREFETCH_THRESHOLD; + + if(m_windowSize != nullsize) + { + movementSize = m_windowSize; + + if(prefetchThreshold >= m_windowSize) + { + prefetchThreshold = std::floor( + static_cast(movementSize) * 0.8 + ); + } + } + + LOG_ERROR_X(movementSize <= 0 || prefetchThreshold <= 0, + "SlidingCacheController::slideBackward", + "Invalid movement size", + false + ); + + QThread::sleep(KOMPLEX_RATE_LIMIT); + m_windowOffset -= movementSize; if(m_windowOffset < 0) @@ -391,6 +507,8 @@ private: if(result.count == 0) { m_windowOffset += movementSize; + + Q_EMIT fetchComplete(); return false; } @@ -419,6 +537,8 @@ private: } } + Q_EMIT fetchComplete(); + return true; } @@ -444,7 +564,7 @@ private: * Total number of results available. Data comes from the return data of * m_fetch */ - qsizetype m_totalCount = std::numeric_limits::infinity(); + qsizetype m_totalCount = nullsize; /** * @brief m_fetch @@ -463,6 +583,12 @@ private: * @brief m_cacheMutex */ mutable QMutex m_cacheMutex; + + /** + * @brief m_windowSize + * Override for window size + */ + qsizetype m_windowSize = nullsize; }; #endif // SLIDINGCACHECONTROLLER_H diff --git a/KomplexHubPlugin/featuredimagesmodel.cpp b/KomplexHubPlugin/featuredimagesmodel.cpp index b22df52..25d3fa8 100644 --- a/KomplexHubPlugin/featuredimagesmodel.cpp +++ b/KomplexHubPlugin/featuredimagesmodel.cpp @@ -2,8 +2,6 @@ FeaturedImagesModel::FeaturedImagesModel(QObject *parent) : QAbstractListModel{parent} { - setState(Loading); - m_paginator = new FeaturedImagesPaginator(this); PaginationNotifier *notifier = static_cast(m_paginator); @@ -37,10 +35,24 @@ FeaturedImagesModel::FeaturedImagesModel(QObject *parent) : QAbstractListModel{p QObject::connect( notifier, - &PaginationNotifier::dataChanged, + &PaginationNotifier::pageChanged, this, &FeaturedImagesModel::resetDataModel ); + + QObject::connect( + notifier, + &PaginationNotifier::fetchComplete, + this, + &FeaturedImagesModel::resetState + ); + + QObject::connect( + notifier, + &PaginationNotifier::fetching, + this, + &FeaturedImagesModel::onPaginatorFetching + ); } auto FeaturedImagesModel::rowCount(const QModelIndex &) const -> int @@ -55,12 +67,12 @@ auto FeaturedImagesModel::rowCount(const QModelIndex &) const -> int auto FeaturedImagesModel::data(const QModelIndex &index, int role) const -> QVariant { - if(index.row() < 0 || m_paginator == nullptr) + if(!boundaryCheck(index.row())) { return {}; } - ImageCache dataPoint = m_data.at(index.row()); + ImageCache dataPoint = m_paginator->at(index.row()); QVariant data; @@ -114,10 +126,12 @@ auto FeaturedImagesModel::index(int row, int column, const QModelIndex &parent) { Q_UNUSED(parent) - if(row < 0 || row >= m_data.count()) + if(!boundaryCheck(row)) + { return {}; + } - return createIndex(row, column, &m_data[row]); + return createIndex(row, column, &m_paginator[row]); } auto FeaturedImagesModel::columnCount(const QModelIndex &) const -> int @@ -158,25 +172,28 @@ auto FeaturedImagesModel::resetState() -> void auto FeaturedImagesModel::resetDataModel() -> void { - setState(Loading); - //invalidate previous model data - beginRemoveRows(QModelIndex(), 0, m_data.count()); - m_data.clear(); + beginRemoveRows(QModelIndex(), 0, m_paginator->count()); endRemoveRows(); //signal new data beginInsertRows(QModelIndex(), 0, m_paginator->count() - 1); - auto dataset = m_paginator->data(); + endInsertRows(); +} - for(const ImageCache& data : std::as_const(dataset)) +auto FeaturedImagesModel::onPaginatorFetching() -> void +{ + setState(Loading); +} + +auto FeaturedImagesModel::boundaryCheck(qsizetype index) const -> bool +{ + if(index < 0 || m_paginator == nullptr || index >= m_paginator->count()) { - m_data.append(data); + return false; } - endInsertRows(); - - setState(Idle); + return true; } auto FeaturedImagesModel::roleNames() const -> QHash @@ -214,7 +231,13 @@ auto FeaturedImagesModel::setResultsPerPage(qsizetype resultsPerPage) -> void { if(m_paginator != nullptr) { - m_paginator->setResultsPerPage(resultsPerPage); + QFuture future = QtConcurrent::run + ( + [this, resultsPerPage] + { + m_paginator->setResultsPerPage(resultsPerPage); + } + ); } } @@ -228,19 +251,31 @@ auto FeaturedImagesModel::totalResults() const -> qsizetype return 0; } -auto FeaturedImagesModel::nextPage() const -> void +auto FeaturedImagesModel::nextPage() -> void { if(m_paginator != nullptr) { - m_paginator->next(); + QFuture future = QtConcurrent::run + ( + [this] + { + m_paginator->next(); + } + ); } } -auto FeaturedImagesModel::previousPage() const -> void +auto FeaturedImagesModel::previousPage() -> void { if(m_paginator != nullptr) { - m_paginator->previous(); + QFuture future = QtConcurrent::run + ( + [this] + { + m_paginator->previous(); + } + ); } } diff --git a/KomplexHubPlugin/featuredimagesmodel.h b/KomplexHubPlugin/featuredimagesmodel.h index 0950291..7645179 100644 --- a/KomplexHubPlugin/featuredimagesmodel.h +++ b/KomplexHubPlugin/featuredimagesmodel.h @@ -35,6 +35,7 @@ #include #include #include +#include #include "paginators/featuredimagespaginator.h" @@ -200,13 +201,13 @@ public: * @brief nextPage * Function for the QML frontend */ - Q_INVOKABLE auto nextPage() const -> void; + Q_INVOKABLE auto nextPage() -> void; /** * @brief previousPage * Function for the QML frontend */ - Q_INVOKABLE auto previousPage() const -> void; + Q_INVOKABLE auto previousPage() -> void; protected: /** @@ -226,8 +227,19 @@ protected: */ auto resetState() -> void; + /** + * @brief resetDataModel + * Clears the data model and creates a new one if data exists + */ auto resetDataModel() -> void; + /** + * @brief onPaginatorFetching + * Sets current state to loading. Triggered when paginator's cache controller + * needs to fetch data from the remote endpoint. + */ + auto onPaginatorFetching() -> void; + signals: auto stateChanged() -> void; auto errorStringChanged() -> void; @@ -238,6 +250,18 @@ signals: auto totalPagesChanged() -> void; private: + /** + * @brief boundaryCheck + * Helper function to check the requested index boundary + * @param index + * @return + */ + auto boundaryCheck(qsizetype index) const -> bool; + + /** + * @brief m_dataRoles + * Data role map that connects ImageSearchModel::DataRole to it's QML accessor name + */ static inline const QHash m_dataRoles = { { @@ -299,8 +323,6 @@ private: FeaturedImagesPaginator *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) diff --git a/KomplexHubPlugin/featuredpacksmodel.cpp b/KomplexHubPlugin/featuredpacksmodel.cpp index 56224bf..5e7507c 100644 --- a/KomplexHubPlugin/featuredpacksmodel.cpp +++ b/KomplexHubPlugin/featuredpacksmodel.cpp @@ -36,10 +36,24 @@ FeaturedPacksModel::FeaturedPacksModel(QObject *parent) : QAbstractListModel{par QObject::connect( notifier, - &PaginationNotifier::dataChanged, + &PaginationNotifier::pageChanged, this, &FeaturedPacksModel::resetDataModel ); + + QObject::connect( + notifier, + &PaginationNotifier::fetchComplete, + this, + &FeaturedPacksModel::resetState + ); + + QObject::connect( + notifier, + &PaginationNotifier::fetching, + this, + &FeaturedPacksModel::onPaginatorFetching + ); } auto FeaturedPacksModel::rowCount(const QModelIndex &) const -> int @@ -54,12 +68,12 @@ auto FeaturedPacksModel::rowCount(const QModelIndex &) const -> int auto FeaturedPacksModel::data(const QModelIndex &index, int role) const -> QVariant { - if(index.row() < 0 || m_paginator == nullptr) + if(index.row() < 0 || m_paginator == nullptr || index.row() >= m_paginator->count()) { return {}; } - WallpaperCache dataPoint = m_data.at(index.row()); + WallpaperCache dataPoint = m_paginator->at(index.row()); QVariant data; @@ -107,10 +121,12 @@ auto FeaturedPacksModel::index(int row, int column, const QModelIndex &parent) c { Q_UNUSED(parent) - if(row < 0 || row >= m_data.count()) + if(row < 0 || row >= m_paginator->count()) + { return {}; + } - return createIndex(row, column, &m_data[row]); + return createIndex(row, column, &m_paginator[row]); } auto FeaturedPacksModel::columnCount(const QModelIndex &) const -> int @@ -141,7 +157,7 @@ auto FeaturedPacksModel::setState(State state) -> void } m_state = state; - emit stateChanged(); + Q_EMIT stateChanged(); } auto FeaturedPacksModel::resetState() -> void @@ -152,22 +168,19 @@ auto FeaturedPacksModel::resetState() -> void auto FeaturedPacksModel::resetDataModel() -> void { //invalidate previous model data - beginRemoveRows(QModelIndex(), 0, m_data.count()); - m_data.clear(); + beginRemoveRows(QModelIndex(), 0, m_paginator->count()); endRemoveRows(); //signal new data beginInsertRows(QModelIndex(), 0, m_paginator->count() - 1); - auto dataset = m_paginator->data(); - - for(const WallpaperCache& data : std::as_const(dataset)) - { - m_data.append(data); - } - endInsertRows(); } +auto FeaturedPacksModel::onPaginatorFetching() -> void +{ + setState(Loading); +} + auto FeaturedPacksModel::roleNames() const -> QHash { return m_dataRoles; @@ -186,7 +199,7 @@ auto FeaturedPacksModel::setErrorString(const QString &errorString) -> void } m_errorString = errorString; - emit errorStringChanged(); + Q_EMIT errorStringChanged(); } auto FeaturedPacksModel::resultsPerPage() const -> qsizetype @@ -203,7 +216,13 @@ auto FeaturedPacksModel::setResultsPerPage(qsizetype resultsPerPage) -> void { if(m_paginator != nullptr) { - m_paginator->setResultsPerPage(resultsPerPage); + QFuture future = QtConcurrent::run + ( + [this, resultsPerPage] + { + m_paginator->setResultsPerPage(resultsPerPage); + } + ); } } @@ -217,19 +236,31 @@ auto FeaturedPacksModel::totalResults() const -> qsizetype return 0; } -auto FeaturedPacksModel::nextPage() const -> void +auto FeaturedPacksModel::nextPage() -> void { if(m_paginator != nullptr) { - m_paginator->next(); + QFuture future = QtConcurrent::run + ( + [this] + { + m_paginator->next(); + } + ); } } -auto FeaturedPacksModel::previousPage() const -> void +auto FeaturedPacksModel::previousPage() -> void { if(m_paginator != nullptr) { - m_paginator->previous(); + QFuture future = QtConcurrent::run + ( + [this] + { + m_paginator->previous(); + } + ); } } diff --git a/KomplexHubPlugin/featuredpacksmodel.h b/KomplexHubPlugin/featuredpacksmodel.h index 2cb931c..24dcab8 100644 --- a/KomplexHubPlugin/featuredpacksmodel.h +++ b/KomplexHubPlugin/featuredpacksmodel.h @@ -35,6 +35,7 @@ #include #include #include +#include #include "paginators/featuredpackspaginator.h" @@ -198,13 +199,13 @@ public: * @brief nextPage * Function for the QML frontend */ - Q_INVOKABLE auto nextPage() const -> void; + Q_INVOKABLE auto nextPage() -> void; /** * @brief previousPage * Function for the QML frontend */ - Q_INVOKABLE auto previousPage() const -> void; + Q_INVOKABLE auto previousPage() -> void; protected: /** @@ -226,6 +227,8 @@ protected: auto resetDataModel() -> void; + auto onPaginatorFetching() -> void; + signals: auto stateChanged() -> void; auto errorStringChanged() -> void; @@ -236,6 +239,10 @@ signals: auto totalPagesChanged() -> void; private: + /** + * @brief m_dataRoles + * Data role map that connects ImageSearchModel::DataRole to it's QML accessor name + */ static inline const QHash m_dataRoles = { { @@ -289,8 +296,6 @@ private: FeaturedPacksPaginator *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) diff --git a/KomplexHubPlugin/featuredvideosmodel.cpp b/KomplexHubPlugin/featuredvideosmodel.cpp index 662ec3b..77ffd6f 100644 --- a/KomplexHubPlugin/featuredvideosmodel.cpp +++ b/KomplexHubPlugin/featuredvideosmodel.cpp @@ -3,8 +3,6 @@ FeaturedVideosModel::FeaturedVideosModel(QObject *parent) : QAbstractListModel{parent} { - setState(Loading); - m_paginator = new FeaturedVideosPaginator(this); PaginationNotifier *notifier = static_cast(m_paginator); @@ -38,10 +36,24 @@ FeaturedVideosModel::FeaturedVideosModel(QObject *parent) : QAbstractListModel{p QObject::connect( notifier, - &PaginationNotifier::dataChanged, + &PaginationNotifier::pageChanged, this, &FeaturedVideosModel::resetDataModel ); + + QObject::connect( + notifier, + &PaginationNotifier::fetchComplete, + this, + &FeaturedVideosModel::resetState + ); + + QObject::connect( + notifier, + &PaginationNotifier::fetching, + this, + &FeaturedVideosModel::onPaginatorFetching + ); } auto FeaturedVideosModel::rowCount(const QModelIndex &) const -> int @@ -56,12 +68,12 @@ auto FeaturedVideosModel::rowCount(const QModelIndex &) const -> int auto FeaturedVideosModel::data(const QModelIndex &index, int role) const -> QVariant { - if(index.row() < 0 || m_paginator == nullptr) + if(index.row() < 0 || m_paginator == nullptr || index.row() >= m_paginator->count()) { return {}; } - VideoCache dataPoint = m_data.at(index.row()); + VideoCache dataPoint = m_paginator->at(index.row()); QVariant data; @@ -103,10 +115,17 @@ auto FeaturedVideosModel::index(int row, int column, const QModelIndex &parent) { Q_UNUSED(parent) - if(row < 0 || row >= m_data.count()) + if(m_paginator == nullptr) + { return {}; + } - return createIndex(row, column, &m_data[row]); + if(row < 0 || row >= m_paginator->count()) + { + return {}; + } + + return createIndex(row, column, &m_paginator[row]); } auto FeaturedVideosModel::columnCount(const QModelIndex &) const -> int @@ -147,25 +166,18 @@ auto FeaturedVideosModel::resetState() -> void auto FeaturedVideosModel::resetDataModel() -> void { - setState(Loading); - //invalidate previous model data - beginRemoveRows(QModelIndex(), 0, m_data.count()); - m_data.clear(); + beginRemoveRows(QModelIndex(), 0, m_paginator->count()); endRemoveRows(); //signal new data beginInsertRows(QModelIndex(), 0, m_paginator->count() - 1); - auto dataset = m_paginator->data(); - - for(const VideoCache& data : std::as_const(dataset)) - { - m_data.append(data); - } - endInsertRows(); +} - setState(Idle); +auto FeaturedVideosModel::onPaginatorFetching() -> void +{ + setState(Loading); } auto FeaturedVideosModel::roleNames() const -> QHash @@ -203,7 +215,13 @@ auto FeaturedVideosModel::setResultsPerPage(qsizetype resultsPerPage) -> void { if(m_paginator != nullptr) { - m_paginator->setResultsPerPage(resultsPerPage); + QFuture future = QtConcurrent::run + ( + [this, resultsPerPage] + { + m_paginator->setResultsPerPage(resultsPerPage); + } + ); } } @@ -217,19 +235,31 @@ auto FeaturedVideosModel::totalResults() const -> qsizetype return 0; } -auto FeaturedVideosModel::nextPage() const -> void +auto FeaturedVideosModel::nextPage() -> void { if(m_paginator != nullptr) { - m_paginator->next(); + QFuture future = QtConcurrent::run + ( + [this] + { + m_paginator->next(); + } + ); } } -auto FeaturedVideosModel::previousPage() const -> void +auto FeaturedVideosModel::previousPage() -> void { if(m_paginator != nullptr) { - m_paginator->previous(); + QFuture future = QtConcurrent::run + ( + [this] + { + m_paginator->previous(); + } + ); } } diff --git a/KomplexHubPlugin/featuredvideosmodel.h b/KomplexHubPlugin/featuredvideosmodel.h index b1d904d..9b0a734 100644 --- a/KomplexHubPlugin/featuredvideosmodel.h +++ b/KomplexHubPlugin/featuredvideosmodel.h @@ -35,6 +35,7 @@ #include #include #include +#include #include "paginators/featuredvideospaginator.h" @@ -196,13 +197,13 @@ public: * @brief nextPage * Function for the QML frontend */ - Q_INVOKABLE auto nextPage() const -> void; + Q_INVOKABLE auto nextPage() -> void; /** * @brief previousPage * Function for the QML frontend */ - Q_INVOKABLE auto previousPage() const -> void; + Q_INVOKABLE auto previousPage() -> void; protected: /** @@ -224,6 +225,8 @@ protected: auto resetDataModel() -> void; + auto onPaginatorFetching() -> void; + signals: auto stateChanged() -> void; auto errorStringChanged() -> void; @@ -234,6 +237,10 @@ signals: auto totalPagesChanged() -> void; private: + /** + * @brief m_dataRoles + * Data role map that connects ImageSearchModel::DataRole to it's QML accessor name + */ static inline const QHash m_dataRoles = { { @@ -279,8 +286,6 @@ private: FeaturedVideosPaginator *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) diff --git a/KomplexHubPlugin/imagesearchmodel.cpp b/KomplexHubPlugin/imagesearchmodel.cpp index 3ab781c..5e8b7b4 100644 --- a/KomplexHubPlugin/imagesearchmodel.cpp +++ b/KomplexHubPlugin/imagesearchmodel.cpp @@ -2,8 +2,6 @@ ImageSearchModel::ImageSearchModel(QObject *parent) : QAbstractListModel{parent} { - setState(Loading); - m_paginator = new ImageSearchPaginator(this); PaginationNotifier *notifier = static_cast(m_paginator); @@ -37,11 +35,25 @@ ImageSearchModel::ImageSearchModel(QObject *parent) : QAbstractListModel{parent} QObject::connect( notifier, - &PaginationNotifier::dataChanged, + &PaginationNotifier::pageChanged, this, &ImageSearchModel::resetDataModel ); + QObject::connect( + notifier, + &PaginationNotifier::fetchComplete, + this, + &ImageSearchModel::resetState + ); + + QObject::connect( + notifier, + &PaginationNotifier::fetching, + this, + &ImageSearchModel::onPaginatorFetching + ); + QObject::connect( m_paginator, &ImageSearchPaginator::queryChanged, @@ -50,6 +62,14 @@ ImageSearchModel::ImageSearchModel(QObject *parent) : QAbstractListModel{parent} ); } +ImageSearchModel::~ImageSearchModel() +{ + if(m_paginator != nullptr) + { + delete m_paginator; + } +} + auto ImageSearchModel::rowCount(const QModelIndex &) const -> int { if(m_paginator != nullptr) @@ -62,12 +82,12 @@ auto ImageSearchModel::rowCount(const QModelIndex &) const -> int auto ImageSearchModel::data(const QModelIndex &index, int role) const -> QVariant { - if(index.row() < 0 || m_paginator == nullptr) + if(index.row() < 0 || m_paginator == nullptr || index.row() >= m_paginator->count()) { return {}; } - ImageCache dataPoint = m_data.at(index.row()); + ImageCache dataPoint = m_paginator->at(index.row()); QVariant data; @@ -112,6 +132,27 @@ auto ImageSearchModel::data(const QModelIndex &index, int role) const -> QVarian case ExtraLargeUrlRole: data = dataPoint.sources.value(QString::fromUtf8("large2x")); break; + case PortraitSizeRole: + data = dataPoint.sourceSizes.value(QString::fromUtf8("portrait")); + break; + case LandscapeSizeRole: + data = dataPoint.sourceSizes.value(QString::fromUtf8("landscape")); + break; + case SmallSizeRole: + data = dataPoint.sourceSizes.value(QString::fromUtf8("small")); + break; + case OriginalSizeRole: + data = dataPoint.sourceSizes.value(QString::fromUtf8("original")); + break; + case MediumSizeRole: + data = dataPoint.sourceSizes.value(QString::fromUtf8("medium")); + break; + case LargeSizeRole: + data = dataPoint.sourceSizes.value(QString::fromUtf8("large")); + break; + case ExtraLargeSizeRole: + data = dataPoint.sourceSizes.value(QString::fromUtf8("large2x")); + break; } return data; @@ -121,10 +162,12 @@ auto ImageSearchModel::index(int row, int column, const QModelIndex &parent) con { Q_UNUSED(parent) - if(row < 0 || row >= m_data.count()) + if(row < 0 || row >= m_paginator->count()) + { return {}; + } - return createIndex(row, column, &m_data[row]); + return createIndex(row, column, &m_paginator->at(row)); } auto ImageSearchModel::columnCount(const QModelIndex &) const -> int @@ -165,33 +208,26 @@ auto ImageSearchModel::resetState() -> void auto ImageSearchModel::resetDataModel() -> void { - setState(Loading); - //invalidate previous model data - beginRemoveRows(QModelIndex(), 0, m_data.count()); - m_data.clear(); + beginRemoveRows(QModelIndex(), 0, m_paginator->count()); 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 +auto ImageSearchModel::onPaginatorFetching() -> void +{ + setState(Loading); +} + +auto ImageSearchModel::hasNextPage() const -> bool { return m_paginator->page() < m_paginator->totalPages(); } -auto ImageSearchModel::hasPreviousPage() -> bool +auto ImageSearchModel::hasPreviousPage() const -> bool { return m_paginator->page() > 0; } @@ -231,7 +267,42 @@ auto ImageSearchModel::setResultsPerPage(qsizetype resultsPerPage) -> void { if(m_paginator != nullptr) { - m_paginator->setResultsPerPage(resultsPerPage); + QFuture future = QtConcurrent::run + ( + [this, resultsPerPage] + { + qsizetype difference = resultsPerPage - m_paginator->resultsPerPage(); + m_paginator->setResultsPerPage(resultsPerPage); + + if(difference > 0) + { + qsizetype firstIndex = m_paginator->resultsPerPage() - difference; + + beginInsertRows + ( + QModelIndex(), + firstIndex, + m_paginator->count() - 1 + ); + + endInsertRows(); + } + + else if(difference < 0) + { + qsizetype firstIndex = m_paginator->resultsPerPage(); + + beginRemoveRows + ( + QModelIndex(), + firstIndex, + firstIndex - difference + ); + + endRemoveRows(); + } + } + ); } } @@ -245,19 +316,31 @@ auto ImageSearchModel::totalResults() const -> qsizetype return 0; } -auto ImageSearchModel::nextPage() const -> void +auto ImageSearchModel::nextPage() -> void { if(m_paginator != nullptr) { - m_paginator->next(); + QFuture future = QtConcurrent::run + ( + [this] + { + m_paginator->next(); + } + ); } } -auto ImageSearchModel::previousPage() const -> void +auto ImageSearchModel::previousPage() -> void { if(m_paginator != nullptr) { - m_paginator->previous(); + QFuture future = QtConcurrent::run + ( + [this] + { + m_paginator->previous(); + } + ); } } @@ -275,7 +358,13 @@ auto ImageSearchModel::setQuery(const QString &query) -> void { if(m_paginator != nullptr) { - m_paginator->setQuery(query); + QFuture future = QtConcurrent::run + ( + [this, query] + { + m_paginator->setQuery(query); + } + ); } } diff --git a/KomplexHubPlugin/imagesearchmodel.h b/KomplexHubPlugin/imagesearchmodel.h index 9977cb9..250a4c0 100644 --- a/KomplexHubPlugin/imagesearchmodel.h +++ b/KomplexHubPlugin/imagesearchmodel.h @@ -35,6 +35,7 @@ #include #include #include +#include #include "paginators/imagesearchpaginator.h" @@ -74,11 +75,19 @@ public: OriginalUrlRole, MediumUrlRole, LargeUrlRole, - ExtraLargeUrlRole + ExtraLargeUrlRole, + PortraitSizeRole, + LandscapeSizeRole, + SmallSizeRole, + OriginalSizeRole, + MediumSizeRole, + LargeSizeRole, + ExtraLargeSizeRole }; Q_ENUM(DataRole) explicit ImageSearchModel(QObject *parent = nullptr); + ~ImageSearchModel(); auto rowCount(const QModelIndex &parent = QModelIndex()) const -> int override; @@ -200,13 +209,13 @@ public: * @brief nextPage * Function for the QML frontend */ - Q_INVOKABLE auto nextPage() const -> void; + Q_INVOKABLE auto nextPage() -> void; /** * @brief previousPage * Function for the QML frontend */ - Q_INVOKABLE auto previousPage() const -> void; + Q_INVOKABLE auto previousPage() -> void; /** * @brief query @@ -217,20 +226,36 @@ public: /** * @brief setQuery - * Sets the current + * Sets the current search query * @param query */ auto setQuery(const QString &query) -> void; + /** + * @brief hasNextPage + * Calculates if there are more pages based on the current page calculation + * @return + */ + auto hasNextPage() const -> bool; + + /** + * @brief hasPreviousPage + * If page is greater than 1, returns true + * @return + */ + auto hasPreviousPage() const -> bool; + protected: /** * @brief setErrorString + * Sets the error string to be displayed to the user * @param errorString */ auto setErrorString(const QString &errorString) -> void; /** * @brief setState + * Sets the current state for QML * @param state */ auto setState(State state) -> void; @@ -240,23 +265,67 @@ protected: */ auto resetState() -> void; + /** + * @brief resetDataModel + * Clears the data model and creates a new one if data exists + */ auto resetDataModel() -> void; - auto hasNextPage() -> bool; - - auto hasPreviousPage() -> bool; + /** + * @brief onPaginatorFetching + * Sets current state to loading. Triggered when paginator's cache controller + * needs to fetch data from the remote endpoint. + */ + auto onPaginatorFetching() -> void; signals: + /** + * @brief stateChanged + * Signaled when reported state changes + */ auto stateChanged() -> void; + + /** + * @brief errorStringChanged + * Signaled when reported error message changes + */ auto errorStringChanged() -> void; + + /** + * @brief pageChanged + * Forwarding signal connected in constructor + */ auto resultsPerPageChanged() -> void; + /** + * @brief pageChanged + * Forwarding signal connected in constructor + */ auto pageChanged() -> void; + + /** + * @brief pageChanged + * Forwarding signal connected in constructor + */ auto totalResultsChanged() -> void; + + /** + * @brief pageChanged + * Forwarding signal connected in constructor + */ auto totalPagesChanged() -> void; + + /** + * @brief pageChanged + * Forwarding signal connected in constructor + */ auto queryChanged() -> void; private: + /** + * @brief m_dataRoles + * Data role map that connects ImageSearchModel::DataRole to it's QML accessor name + */ static inline const QHash m_dataRoles = { { @@ -310,6 +379,34 @@ private: { static_cast(ExtraLargeUrlRole), QByteArray("extraLarge") + }, + { + static_cast(PortraitSizeRole), + QByteArray("portraitSize") + }, + { + static_cast(LandscapeSizeRole), + QByteArray("landscapeSize") + }, + { + static_cast(SmallSizeRole), + QByteArray("smallSize") + }, + { + static_cast(OriginalSizeRole), + QByteArray("originalSize") + }, + { + static_cast(MediumSizeRole), + QByteArray("mediumSize") + }, + { + static_cast(LargeSizeRole), + QByteArray("largeSize") + }, + { + static_cast(ExtraLargeSizeRole), + QByteArray("extraLargeSize") } }; State m_state = Idle; @@ -318,8 +415,6 @@ private: 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) diff --git a/KomplexHubPlugin/newestpacksmodel.cpp b/KomplexHubPlugin/newestpacksmodel.cpp index fe63cce..75e24b9 100644 --- a/KomplexHubPlugin/newestpacksmodel.cpp +++ b/KomplexHubPlugin/newestpacksmodel.cpp @@ -38,10 +38,24 @@ NewestPacksModel::NewestPacksModel(QObject *parent) : QAbstractListModel{parent} QObject::connect( notifier, - &PaginationNotifier::dataChanged, + &PaginationNotifier::pageChanged, this, &NewestPacksModel::resetDataModel ); + + QObject::connect( + notifier, + &PaginationNotifier::fetchComplete, + this, + &NewestPacksModel::resetState + ); + + QObject::connect( + notifier, + &PaginationNotifier::fetching, + this, + &NewestPacksModel::onPaginatorFetching + ); } auto NewestPacksModel::rowCount(const QModelIndex &) const -> int @@ -56,12 +70,12 @@ auto NewestPacksModel::rowCount(const QModelIndex &) const -> int auto NewestPacksModel::data(const QModelIndex &index, int role) const -> QVariant { - if(index.row() < 0 || m_paginator == nullptr) + if(index.row() < 0 || m_paginator == nullptr || index.row() >= m_paginator->count()) { return {}; } - WallpaperCache dataPoint = m_data.at(index.row()); + WallpaperCache dataPoint = m_paginator->at(index.row()); QVariant data; @@ -109,10 +123,10 @@ auto NewestPacksModel::index(int row, int column, const QModelIndex &parent) con { Q_UNUSED(parent) - if(row < 0 || row >= m_data.count()) + if(row < 0 || row >= m_paginator->count()) return {}; - return createIndex(row, column, &m_data[row]); + return createIndex(row, column, &m_paginator[row]); } auto NewestPacksModel::columnCount(const QModelIndex &) const -> int @@ -153,23 +167,18 @@ auto NewestPacksModel::resetState() -> void auto NewestPacksModel::resetDataModel() -> void { - setState(Loading); //invalidate previous model data - beginRemoveRows(QModelIndex(), 0, m_data.count()); - m_data.clear(); + beginRemoveRows(QModelIndex(), 0, m_paginator->count()); endRemoveRows(); //signal new data beginInsertRows(QModelIndex(), 0, m_paginator->count() - 1); - auto dataset = m_paginator->data(); - - for(const WallpaperCache& data : std::as_const(dataset)) - { - m_data.append(data); - } - endInsertRows(); - setState(Idle); +} + +auto NewestPacksModel::onPaginatorFetching() -> void +{ + setState(Loading); } auto NewestPacksModel::roleNames() const -> QHash @@ -225,7 +234,13 @@ auto NewestPacksModel::nextPage() const -> void { if(m_paginator != nullptr) { - m_paginator->next(); + QFuture future = QtConcurrent::run + ( + [this] + { + m_paginator->next(); + } + ); } } @@ -233,7 +248,13 @@ auto NewestPacksModel::previousPage() const -> void { if(m_paginator != nullptr) { - m_paginator->previous(); + QFuture future = QtConcurrent::run + ( + [this] + { + m_paginator->previous(); + } + ); } } diff --git a/KomplexHubPlugin/newestpacksmodel.h b/KomplexHubPlugin/newestpacksmodel.h index d99b67b..f87d796 100644 --- a/KomplexHubPlugin/newestpacksmodel.h +++ b/KomplexHubPlugin/newestpacksmodel.h @@ -35,6 +35,7 @@ #include #include #include +#include #include "paginators/newestpackspaginator.h" @@ -236,6 +237,8 @@ protected: */ auto resetDataModel() -> void; + auto onPaginatorFetching() -> void; + signals: auto stateChanged() -> void; auto errorStringChanged() -> void; @@ -299,8 +302,6 @@ private: NewestPacksPaginator *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) diff --git a/KomplexHubPlugin/packsearchmodel.cpp b/KomplexHubPlugin/packsearchmodel.cpp new file mode 100644 index 0000000..83aa256 --- /dev/null +++ b/KomplexHubPlugin/packsearchmodel.cpp @@ -0,0 +1,328 @@ +#include "packsearchmodel.h" + +PackSearchModel::PackSearchModel(QObject *parent) : QAbstractListModel{parent} +{ + m_paginator = new PackSearchPaginator(this); + PaginationNotifier *notifier = static_cast(m_paginator); + + QObject::connect( + notifier, + &PaginationNotifier::resultsPerPageChanged, + this, + &PackSearchModel::resultsPerPageChanged + ); + + QObject::connect( + notifier, + &PaginationNotifier::totalResultsChanged, + this, + &PackSearchModel::totalResultsChanged + ); + + QObject::connect( + notifier, + &PaginationNotifier::totalPagesChanged, + this, + &PackSearchModel::totalPagesChanged + ); + + QObject::connect( + notifier, + &PaginationNotifier::pageChanged, + this, + &PackSearchModel::pageChanged + ); + + QObject::connect( + notifier, + &PaginationNotifier::pageChanged, + this, + &PackSearchModel::resetDataModel + ); + + QObject::connect( + notifier, + &PaginationNotifier::fetchComplete, + this, + &PackSearchModel::resetState + ); + + QObject::connect( + notifier, + &PaginationNotifier::fetching, + this, + &PackSearchModel::onPaginatorFetching + ); + + QObject::connect( + m_paginator, + &PaginationNotifier::queryChanged, + this, + &PackSearchModel::queryChanged + ); +} + +auto PackSearchModel::rowCount(const QModelIndex &) const -> int +{ + if(m_paginator != nullptr) + { + return m_paginator->count(); + } + + return 0; +} + +auto PackSearchModel::data(const QModelIndex &index, int role) const -> QVariant +{ + if(m_paginator == nullptr) + { + return {}; + } + + if(index.row() < 0 || index.row() >= m_paginator->count() ) + { + return {}; + } + + WallpaperCache dataPoint = m_paginator->at(index.row()); + + QVariant data; + + switch(static_cast(role)) + { + case UuidRole: + data = dataPoint.uuid; + break; + case AuthorRole: + data = dataPoint.author; + break; + case DescriptionRole: + data = dataPoint.description; + break; + case NameRole: + data = dataPoint.name; + break; + case ThumbnailRole: + data = dataPoint.thumbnail; + break; + case CreatedDateRole: + data = dataPoint.createdDate; + break; + case AuthorIdRole: + data = dataPoint.authorId; + break; + case PriceRole: + data = dataPoint.price; + break; + case CurrencyRole: + data = dataPoint.currency; + break; + case DownloadCountRole: + data = dataPoint.downloadCount; + break; + case TypeRole: + data = dataPoint.type; + break; + } + + return data; +} + +auto PackSearchModel::index(int row, int column, const QModelIndex &parent) const -> QModelIndex +{ + Q_UNUSED(parent) + + if(row < 0 || row >= m_paginator->count()) + return {}; + + return createIndex(row, column, &m_paginator[row]); +} + +auto PackSearchModel::columnCount(const QModelIndex &) const -> int +{ + return 0; +} + +auto PackSearchModel::parent(const QModelIndex &) const -> QModelIndex +{ + return {}; +} + +auto PackSearchModel::setData(const QModelIndex &, const QVariant &, int) -> bool +{ + return false; +} + +auto PackSearchModel::state() const -> PackSearchModel::State +{ + return m_state; +} + +auto PackSearchModel::setState(State state) -> void +{ + if (m_state == state) + { + return; + } + + m_state = state; + emit stateChanged(); +} + +auto PackSearchModel::resetState() -> void +{ + setState(Idle); +} + +auto PackSearchModel::resetDataModel() -> void +{ + //invalidate previous model data + beginRemoveRows(QModelIndex(), 0, m_paginator->count()); + endRemoveRows(); + + //signal new data + beginInsertRows(QModelIndex(), 0, m_paginator->count() - 1); + endInsertRows(); +} + +auto PackSearchModel::hasNextPage() const -> bool +{ + return m_paginator->page() < m_paginator->totalPages(); +} + +auto PackSearchModel::hasPreviousPage() const -> bool +{ + return m_paginator->page() > 0; +} + +auto PackSearchModel::onPaginatorFetching() -> void +{ + setState(Loading); +} + +auto PackSearchModel::roleNames() const -> QHash +{ + return m_dataRoles; +} + +auto PackSearchModel::errorString() const -> QString +{ + return m_errorString; +} + +auto PackSearchModel::setErrorString(const QString &errorString) -> void +{ + if (m_errorString == errorString) + { + return; + } + + m_errorString = errorString; + emit errorStringChanged(); +} + +auto PackSearchModel::resultsPerPage() const -> qsizetype +{ + if(m_paginator != nullptr) + { + return m_paginator->resultsPerPage(); + } + + return 0; +} + +auto PackSearchModel::setResultsPerPage(qsizetype resultsPerPage) -> void +{ + if(m_paginator != nullptr) + { + QFuture future = QtConcurrent::run + ( + [this, resultsPerPage] + { + m_paginator->setResultsPerPage(resultsPerPage); + } + ); + } +} + +auto PackSearchModel::totalResults() const -> qsizetype +{ + if(m_paginator != nullptr) + { + return m_paginator->totalResults(); + } + + return 0; +} + +auto PackSearchModel::nextPage() -> void +{ + if(m_paginator != nullptr) + { + QFuture future = QtConcurrent::run + ( + [this] + { + m_paginator->next(); + } + ); + } +} + +auto PackSearchModel::previousPage() -> void +{ + if(m_paginator != nullptr) + { + QFuture future = QtConcurrent::run + ( + [this] + { + m_paginator->previous(); + } + ); + } +} + +auto PackSearchModel::query() const -> QString +{ + if(m_paginator != nullptr) + { + return m_paginator->query(); + } + + return {}; +} + +auto PackSearchModel::setQuery(const QString &query) -> void +{ + if(m_paginator != nullptr) + { + QFuture future = QtConcurrent::run + ( + [this, query] + { + m_paginator->setQuery(query); + } + ); + } +} + +qsizetype PackSearchModel::page() const +{ + if(m_paginator != nullptr) + { + return m_paginator->page(); + } + + return 0; +} + +auto PackSearchModel::totalPages() const -> qsizetype +{ + if(m_paginator != nullptr) + { + return m_paginator->totalPages(); + } + + return 0; +} diff --git a/KomplexHubPlugin/packsearchmodel.h b/KomplexHubPlugin/packsearchmodel.h new file mode 100644 index 0000000..f957ee5 --- /dev/null +++ b/KomplexHubPlugin/packsearchmodel.h @@ -0,0 +1,383 @@ +/* + * 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 PackSearchModel_H +#define PackSearchModel_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "paginators/packsearchpaginator.h" + +/** + * @brief The PackSearchModel class + */ +class KOMPLEX_EXPORT PackSearchModel : 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, + DescriptionRole, + NameRole, + ThumbnailRole, + CreatedDateRole, + PriceRole, + CurrencyRole, + DownloadCountRole, + TypeRole + }; + Q_ENUM(DataRole) + + explicit PackSearchModel(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() -> void; + + /** + * @brief previousPage + * Function for the QML frontend + */ + Q_INVOKABLE auto previousPage() -> void; + + /** + * @brief query + * Current search query + * @return + */ + auto query() const -> QString; + + /** + * @brief setQuery + * Sets the current search query + * @param query + */ + auto setQuery(const QString &query) -> void; + + /** + * @brief hasNextPage + * Calculates if there are more pages based on the current page calculation + * @return + */ + auto hasNextPage() const -> bool; + + /** + * @brief hasPreviousPage + * If page is greater than 1, returns true + * @return + */ + auto hasPreviousPage() const -> bool; + +protected: + + /** + * @brief onPaginatorFetching + * Sets current state to loading. Triggered when paginator's cache controller + * needs to fetch data from the remote endpoint. + */ + auto onPaginatorFetching() -> void; + + /** + * @brief setErrorString + * @param errorString + */ + auto setErrorString(const QString &errorString) -> void; + + /** + * @brief setState + * @param state + */ + auto setState(State state) -> void; + + /** + * @brief resetState + */ + auto resetState() -> void; + + /** + * @brief resetDataModel + * Clears the data model and creates a new one if data exists + */ + auto resetDataModel() -> void; + +signals: + /** + * @brief stateChanged + * Signaled when reported state changes + */ + auto stateChanged() -> void; + + /** + * @brief errorStringChanged + * Signaled when reported error message changes + */ + auto errorStringChanged() -> void; + + /** + * @brief pageChanged + * Forwarding signal connected in constructor + */ + auto resultsPerPageChanged() -> void; + + /** + * @brief pageChanged + * Forwarding signal connected in constructor + */ + auto pageChanged() -> void; + + /** + * @brief pageChanged + * Forwarding signal connected in constructor + */ + auto totalResultsChanged() -> void; + + /** + * @brief pageChanged + * Forwarding signal connected in constructor + */ + auto totalPagesChanged() -> void; + + /** + * @brief pageChanged + * Forwarding signal connected in constructor + */ + auto queryChanged() -> void; + +private: + /** + * Data role map that connects PackSearchModel::DataRole to it's QML accessor name + */ + static inline const QHash m_dataRoles = + { + { + static_cast(UuidRole), + QByteArray("uuid") + }, + { + static_cast(AuthorRole), + QByteArray("author") + }, + { + static_cast(AuthorIdRole), + QByteArray("authorId") + }, + { + static_cast(DescriptionRole), + QByteArray("description") + }, + { + static_cast(NameRole), + QByteArray("name") + }, + { + static_cast(ThumbnailRole), + QByteArray("thumbnail") + }, + { + static_cast(CreatedDateRole), + QByteArray("createdDate") + }, + { + static_cast(PriceRole), + QByteArray("price") + }, + { + static_cast(CurrencyRole), + QByteArray("currency") + }, + { + static_cast(DownloadCountRole), + QByteArray("downloadCount") + }, + { + static_cast(TypeRole), + QByteArray("type") + } + }; + + State m_state = Idle; + + QString m_errorString = QString(); + + PackSearchPaginator *m_paginator = nullptr; + + 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(PackSearchModel) + +#endif // PackSearchModel_H diff --git a/KomplexHubPlugin/paginators/featuredcubemapspaginator.h b/KomplexHubPlugin/paginators/featuredcubemapspaginator.h index c4d3d01..e7e9731 100644 --- a/KomplexHubPlugin/paginators/featuredcubemapspaginator.h +++ b/KomplexHubPlugin/paginators/featuredcubemapspaginator.h @@ -39,6 +39,8 @@ public: setUri( QString::fromUtf8(KOMPLEX_ENDPOINT_CUBEMAPS_FEATURED) ); + + controller()->setWindowSize(20); } }; diff --git a/KomplexHubPlugin/paginators/featuredimagespaginator.h b/KomplexHubPlugin/paginators/featuredimagespaginator.h index f3490f4..3b9dfbb 100644 --- a/KomplexHubPlugin/paginators/featuredimagespaginator.h +++ b/KomplexHubPlugin/paginators/featuredimagespaginator.h @@ -39,6 +39,8 @@ public: setUri( QString::fromUtf8(KOMPLEX_ENDPOINT_IMAGES_FEATURED) ); + + controller()->setWindowSize(20); } }; diff --git a/KomplexHubPlugin/paginators/featuredpackspaginator.h b/KomplexHubPlugin/paginators/featuredpackspaginator.h index 9689473..00a1741 100644 --- a/KomplexHubPlugin/paginators/featuredpackspaginator.h +++ b/KomplexHubPlugin/paginators/featuredpackspaginator.h @@ -39,6 +39,8 @@ public: setUri( QString::fromUtf8(KOMPLEX_ENDPOINT_PACKS_FEATURED) ); + + controller()->setWindowSize(20); } }; diff --git a/KomplexHubPlugin/paginators/featuredshaderspaginator.h b/KomplexHubPlugin/paginators/featuredshaderspaginator.h index cf7797e..6ddb9ac 100644 --- a/KomplexHubPlugin/paginators/featuredshaderspaginator.h +++ b/KomplexHubPlugin/paginators/featuredshaderspaginator.h @@ -39,6 +39,8 @@ public: setUri( QString::fromUtf8(KOMPLEX_ENDPOINT_SHADERS_FEATURED) ); + + controller()->setWindowSize(20); } }; diff --git a/KomplexHubPlugin/paginators/featuredvideospaginator.h b/KomplexHubPlugin/paginators/featuredvideospaginator.h index f06ac77..2f57252 100644 --- a/KomplexHubPlugin/paginators/featuredvideospaginator.h +++ b/KomplexHubPlugin/paginators/featuredvideospaginator.h @@ -39,6 +39,8 @@ public: setUri( QString::fromUtf8(KOMPLEX_ENDPOINT_VIDEOS_FEATURED) ); + + controller()->setWindowSize(20); } }; diff --git a/KomplexHubPlugin/paginators/imagepaginator.h b/KomplexHubPlugin/paginators/imagepaginator.h index 1716f5a..50382da 100644 --- a/KomplexHubPlugin/paginators/imagepaginator.h +++ b/KomplexHubPlugin/paginators/imagepaginator.h @@ -62,46 +62,12 @@ public: return m_uri; } - /** - * @brief query - * Current query string, if the API Endpoint supports queries - * @return - */ - [[nodiscard]] - auto query() const -> QString - { - return m_query; - } - - /** - * @brief setQuery - * Sets the new query string and updates the controller - * @param query - */ - auto setQuery(const QString &query) -> void - { - if(query == m_query) - { - return; - } - - m_query = query; - Q_EMIT queryChanged(); - - reset(); - } - signals: /** * @brief uriChanged */ auto uriChanged() -> void; - /** - * @brief queryChanged - */ - auto queryChanged() -> void; - protected: /** * @brief fetch @@ -124,6 +90,11 @@ protected: {} ); + if(queryable() && query().isEmpty()) + { + return {}; + } + QUrl url( QString::fromUtf8("%1/%2/%3").arg( KOMPLEX_API_HOST, @@ -138,7 +109,7 @@ protected: { { QByteArray("query"), - m_query.toUtf8() + query().toUtf8() }, { QByteArray("offset"), @@ -181,13 +152,28 @@ protected: const QVariantMap sources = resultObject.value(QString::fromUtf8("src")).toObject().toVariantMap(); QMap sourceMap; + QMap sizeMap; QMapIterator sourceIterator(sources); while(sourceIterator.hasNext()) { sourceIterator.next(); - sourceMap.insert(sourceIterator.key(), sourceIterator.value().toString()); + + QString url = sourceIterator.value().toString(); + sourceMap.insert(sourceIterator.key(), url); + + auto matches = s_sizeExpression.match(url); + + if(matches.hasMatch()) + { + QString size = matches.captured(); + size.remove(QString::fromUtf8("w=")); + size.remove(QString::fromUtf8("h=")); + size.replace(QLatin1Char('&'), QLatin1Char('x')); + + sizeMap.insert(sourceIterator.key(), std::move(size)); + } } fetchedResults.append( @@ -202,6 +188,7 @@ protected: resultObject.value(QString::fromUtf8("photographer_url")).toString(), resultObject.value(QString::fromUtf8("photographer_id")).toInteger(), std::move(sourceMap), + std::move(sizeMap), resultObject.value(QString::fromUtf8("url")).toString() } ); @@ -307,10 +294,13 @@ private: QString m_uri; /** - * @brief m_query - * The query string used in fetch operations + * @brief s_sizeExpression + * Regular expression used to extract size data from the url */ - QString m_query; + inline static QRegularExpression s_sizeExpression = QRegularExpression + ( + QString::fromUtf8("[h]\\=\\d{1,}\\&[w]\\=\\d{1,}") + ); }; #endif // IMAGEPAGINATOR_H diff --git a/KomplexHubPlugin/paginators/imagesearchpaginator.h b/KomplexHubPlugin/paginators/imagesearchpaginator.h index 473fe78..4c17740 100644 --- a/KomplexHubPlugin/paginators/imagesearchpaginator.h +++ b/KomplexHubPlugin/paginators/imagesearchpaginator.h @@ -41,6 +41,8 @@ public: setUri( QString::fromUtf8(KOMPLEX_ENDPOINT_IMAGES_SEARCH) ); + + setQueryable(true); } }; diff --git a/KomplexHubPlugin/paginators/newestpackspaginator.h b/KomplexHubPlugin/paginators/newestpackspaginator.h index c97bdb8..7c63ab6 100644 --- a/KomplexHubPlugin/paginators/newestpackspaginator.h +++ b/KomplexHubPlugin/paginators/newestpackspaginator.h @@ -39,6 +39,8 @@ public: setUri( QString::fromUtf8(KOMPLEX_ENDPOINT_PACKS_NEWEST) ); + + controller()->setWindowSize(20); } }; diff --git a/KomplexHubPlugin/paginators/searchpackspaginator.h b/KomplexHubPlugin/paginators/packsearchpaginator.h similarity index 80% rename from KomplexHubPlugin/paginators/searchpackspaginator.h rename to KomplexHubPlugin/paginators/packsearchpaginator.h index dd65474..6b3ada0 100644 --- a/KomplexHubPlugin/paginators/searchpackspaginator.h +++ b/KomplexHubPlugin/paginators/packsearchpaginator.h @@ -16,8 +16,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see */ -#ifndef SEARCHPACKSPAGINATOR_H -#define SEARCHPACKSPAGINATOR_H +#ifndef PACKSEARCHPAGINATOR_H +#define PACKSEARCHPAGINATOR_H #include #include @@ -31,15 +31,17 @@ #include "wallpaperpaginator.h" -class KOMPLEX_EXPORT SearchPacksPaginator : public WallpaperPaginator +class KOMPLEX_EXPORT PackSearchPaginator : public WallpaperPaginator { public: - explicit SearchPacksPaginator(QObject *parent = nullptr) : WallpaperPaginator(parent) + explicit PackSearchPaginator(QObject *parent = nullptr) : WallpaperPaginator(parent) { setUri( QString::fromUtf8(KOMPLEX_ENDPOINT_PACKS_SEARCH) ); + + setQueryable(true); } }; -#endif // SEARCHPACKSPAGINATOR_H +#endif // PACKSEARCHPAGINATOR_H diff --git a/KomplexHubPlugin/paginators/videopaginator.h b/KomplexHubPlugin/paginators/videopaginator.h index 2dd9b1c..566fa35 100644 --- a/KomplexHubPlugin/paginators/videopaginator.h +++ b/KomplexHubPlugin/paginators/videopaginator.h @@ -62,47 +62,12 @@ public: return m_uri; } - /** - * @brief query - * Current query string, if the API Endpoint supports queries - * @return - */ - [[nodiscard]] - auto query() const -> QString - { - return m_query; - } - - /** - * @brief setQuery - * Sets the new query string and updates the controller - * @param query - */ - auto setQuery(const QString &query) -> void - { - if(query == m_query) - { - return; - } - - m_query = query; - Q_EMIT queryChanged(); - - reset(); - setOffset(0); - } - signals: /** * @brief uriChanged */ auto uriChanged() -> void; - /** - * @brief queryChanged - */ - auto queryChanged() -> void; - protected: /** * @brief fetch @@ -125,6 +90,11 @@ protected: {} ); + if(queryable() && query().isEmpty()) + { + return {}; + } + QUrl url( QString::fromUtf8("%1/%2/%3").arg( KOMPLEX_API_HOST, @@ -139,7 +109,7 @@ protected: { { QByteArray("query"), - m_query.toUtf8() + query().toUtf8() }, { QByteArray("offset"), @@ -258,7 +228,12 @@ protected: QNetworkReply *reply = manager->post(request, nullptr); QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - QObject::connect(reply, &QNetworkReply::errorOccurred, &loop, + + QObject::connect + ( + reply, + &QNetworkReply::errorOccurred, + &loop, [&loop](QNetworkReply::NetworkError error) -> void { loop.quit(); diff --git a/KomplexHubPlugin/paginators/videosearchpaginator.h b/KomplexHubPlugin/paginators/videosearchpaginator.h index 491ebe2..32e1f43 100644 --- a/KomplexHubPlugin/paginators/videosearchpaginator.h +++ b/KomplexHubPlugin/paginators/videosearchpaginator.h @@ -39,6 +39,8 @@ public: setUri( QString::fromUtf8(KOMPLEX_ENDPOINT_VIDEOS_SEARCH) ); + + setQueryable(true); } }; diff --git a/KomplexHubPlugin/paginators/wallpaperpaginator.h b/KomplexHubPlugin/paginators/wallpaperpaginator.h index de2cedf..e9cbe1e 100644 --- a/KomplexHubPlugin/paginators/wallpaperpaginator.h +++ b/KomplexHubPlugin/paginators/wallpaperpaginator.h @@ -98,11 +98,6 @@ signals: */ auto uriChanged() -> void; - /** - * @brief queryChanged - */ - auto queryChanged() -> void; - protected: /** * @brief fetch @@ -125,6 +120,11 @@ protected: {} ); + if(queryable() && query().isEmpty()) + { + return {}; + } + QUrl url( QString::fromUtf8("%1/%2/%3").arg( KOMPLEX_API_HOST, diff --git a/KomplexHubPlugin/videosearchmodel.cpp b/KomplexHubPlugin/videosearchmodel.cpp index 7e696a4..737847e 100644 --- a/KomplexHubPlugin/videosearchmodel.cpp +++ b/KomplexHubPlugin/videosearchmodel.cpp @@ -2,8 +2,6 @@ VideoSearchModel::VideoSearchModel(QObject *parent) : QAbstractListModel{parent} { - setState(Loading); - m_paginator = new VideoSearchPaginator(this); PaginationNotifier *notifier = static_cast(m_paginator); @@ -37,11 +35,25 @@ VideoSearchModel::VideoSearchModel(QObject *parent) : QAbstractListModel{parent} QObject::connect( notifier, - &PaginationNotifier::dataChanged, + &PaginationNotifier::pageChanged, this, &VideoSearchModel::resetDataModel ); + QObject::connect( + notifier, + &PaginationNotifier::fetchComplete, + this, + &VideoSearchModel::resetState + ); + + QObject::connect( + notifier, + &PaginationNotifier::fetching, + this, + &VideoSearchModel::onPaginatorFetching + ); + QObject::connect( m_paginator, &VideoSearchPaginator::queryChanged, @@ -62,12 +74,17 @@ auto VideoSearchModel::rowCount(const QModelIndex &) const -> int auto VideoSearchModel::data(const QModelIndex &index, int role) const -> QVariant { - if(index.row() < 0 || m_paginator == nullptr) + if(m_paginator == nullptr) { return {}; } - VideoCache dataPoint = m_data.at(index.row()); + if(index.row() < 0 || index.row() >= m_paginator->count() ) + { + return {}; + } + + VideoCache dataPoint = m_paginator->at(index.row()); QVariant data; @@ -109,10 +126,10 @@ auto VideoSearchModel::index(int row, int column, const QModelIndex &parent) con { Q_UNUSED(parent) - if(row < 0 || row >= m_data.count()) + if(row < 0 || row >= m_paginator->count()) return {}; - return createIndex(row, column, &m_data[row]); + return createIndex(row, column, &m_paginator[row]); } auto VideoSearchModel::columnCount(const QModelIndex &) const -> int @@ -153,37 +170,30 @@ auto VideoSearchModel::resetState() -> void auto VideoSearchModel::resetDataModel() -> void { - setState(Loading); - //invalidate previous model data - beginRemoveRows(QModelIndex(), 0, m_data.count()); - m_data.clear(); + beginRemoveRows(QModelIndex(), 0, m_paginator->count()); endRemoveRows(); //signal new data beginInsertRows(QModelIndex(), 0, m_paginator->count() - 1); - auto dataset = m_paginator->data(); - - for(const VideoCache& data : std::as_const(dataset)) - { - m_data.append(data); - } - endInsertRows(); - - setState(Idle); } -auto VideoSearchModel::hasNextPage() -> bool +auto VideoSearchModel::hasNextPage() const -> bool { return m_paginator->page() < m_paginator->totalPages(); } -auto VideoSearchModel::hasPreviousPage() -> bool +auto VideoSearchModel::hasPreviousPage() const -> bool { return m_paginator->page() > 0; } +auto VideoSearchModel::onPaginatorFetching() -> void +{ + setState(Loading); +} + auto VideoSearchModel::roleNames() const -> QHash { return m_dataRoles; @@ -219,7 +229,13 @@ auto VideoSearchModel::setResultsPerPage(qsizetype resultsPerPage) -> void { if(m_paginator != nullptr) { - m_paginator->setResultsPerPage(resultsPerPage); + QFuture future = QtConcurrent::run + ( + [this, resultsPerPage] + { + m_paginator->setResultsPerPage(resultsPerPage); + } + ); } } @@ -233,19 +249,31 @@ auto VideoSearchModel::totalResults() const -> qsizetype return 0; } -auto VideoSearchModel::nextPage() const -> void +auto VideoSearchModel::nextPage() -> void { if(m_paginator != nullptr) { - m_paginator->next(); + QFuture future = QtConcurrent::run + ( + [this] + { + m_paginator->next(); + } + ); } } -auto VideoSearchModel::previousPage() const -> void +auto VideoSearchModel::previousPage() -> void { if(m_paginator != nullptr) { - m_paginator->previous(); + QFuture future = QtConcurrent::run + ( + [this] + { + m_paginator->previous(); + } + ); } } @@ -263,7 +291,13 @@ auto VideoSearchModel::setQuery(const QString &query) -> void { if(m_paginator != nullptr) { - m_paginator->setQuery(query); + QFuture future = QtConcurrent::run + ( + [this, query] + { + m_paginator->setQuery(query); + } + ); } } diff --git a/KomplexHubPlugin/videosearchmodel.h b/KomplexHubPlugin/videosearchmodel.h index 2c3c470..a756b73 100644 --- a/KomplexHubPlugin/videosearchmodel.h +++ b/KomplexHubPlugin/videosearchmodel.h @@ -35,6 +35,7 @@ #include #include #include +#include #include "paginators/videosearchpaginator.h" @@ -196,13 +197,13 @@ public: * @brief nextPage * Function for the QML frontend */ - Q_INVOKABLE auto nextPage() const -> void; + Q_INVOKABLE auto nextPage() -> void; /** * @brief previousPage * Function for the QML frontend */ - Q_INVOKABLE auto previousPage() const -> void; + Q_INVOKABLE auto previousPage() -> void; /** * @brief query @@ -213,12 +214,34 @@ public: /** * @brief setQuery - * Sets the current + * Sets the current search query * @param query */ auto setQuery(const QString &query) -> void; + /** + * @brief hasNextPage + * Calculates if there are more pages based on the current page calculation + * @return + */ + auto hasNextPage() const -> bool; + + /** + * @brief hasPreviousPage + * If page is greater than 1, returns true + * @return + */ + auto hasPreviousPage() const -> bool; + protected: + + /** + * @brief onPaginatorFetching + * Sets current state to loading. Triggered when paginator's cache controller + * needs to fetch data from the remote endpoint. + */ + auto onPaginatorFetching() -> void; + /** * @brief setErrorString * @param errorString @@ -236,23 +259,59 @@ protected: */ auto resetState() -> void; + /** + * @brief resetDataModel + * Clears the data model and creates a new one if data exists + */ auto resetDataModel() -> void; - auto hasNextPage() -> bool; - - auto hasPreviousPage() -> bool; - signals: + /** + * @brief stateChanged + * Signaled when reported state changes + */ auto stateChanged() -> void; + + /** + * @brief errorStringChanged + * Signaled when reported error message changes + */ auto errorStringChanged() -> void; + + /** + * @brief pageChanged + * Forwarding signal connected in constructor + */ auto resultsPerPageChanged() -> void; + /** + * @brief pageChanged + * Forwarding signal connected in constructor + */ auto pageChanged() -> void; + + /** + * @brief pageChanged + * Forwarding signal connected in constructor + */ auto totalResultsChanged() -> void; + + /** + * @brief pageChanged + * Forwarding signal connected in constructor + */ auto totalPagesChanged() -> void; + + /** + * @brief pageChanged + * Forwarding signal connected in constructor + */ auto queryChanged() -> void; private: + /** + * Data role map that connects VideoSearchModel::DataRole to it's QML accessor name + */ static inline const QHash m_dataRoles = { { @@ -298,8 +357,6 @@ private: VideoSearchPaginator *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) diff --git a/images/icons/pexels-icon-filled-256.svg b/images/icons/pexels-icon-filled-256.svg new file mode 100644 index 0000000..aa2632b --- /dev/null +++ b/images/icons/pexels-icon-filled-256.svg @@ -0,0 +1,7 @@ + + + + + + +