From e5627742d2761361facca7bcee763a37b73aab09 Mon Sep 17 00:00:00 2001 From: Digital Artifex <7929434+DigitalArtifex@users.noreply.github.com> Date: Wed, 5 Aug 2026 19:58:24 -0400 Subject: [PATCH] Large update. Added Video, Image and Live item views. Added Live search --- App/CMakeLists.txt | 3 +- App/main.cpp | 34 + CMakeLists.txt | 10 +- KomplexHubContent/MainScreen.qml | 14 +- KomplexHubContent/pages/HomePage.qml | 116 ++- KomplexHubContent/pages/ImageSearchPage.qml | 42 +- KomplexHubContent/pages/LiveSearchPage.qml | 67 +- KomplexHubContent/pages/VideoSearchPage.qml | 86 +- KomplexHubModule/Controls/CMakeLists.txt | 3 + KomplexHubModule/Controls/ImageView.qml | 94 +- KomplexHubModule/Controls/PackView.qml | 177 ++++ .../Controls/SearchResultItem.qml | 47 +- KomplexHubModule/Controls/SquareButton.qml | 2 +- KomplexHubModule/Controls/Throbber.qml | 259 +++--- KomplexHubModule/Controls/VideoView.qml | 191 ++++ KomplexHubPlugin/CMakeLists.txt | 73 +- KomplexHubPlugin/common/coreservices.cpp | 12 + KomplexHubPlugin/common/coreservices.h | 3 + KomplexHubPlugin/common/exceptions.h | 28 + KomplexHubPlugin/common/komplex_global.h | 3 +- KomplexHubPlugin/common/paginator.h | 63 ++ KomplexHubPlugin/common/shaderpackmetadata.h | 138 +++ KomplexHubPlugin/common/shadertoymetadata.h | 108 +++ .../common/slidingcachecontroller.h | 5 +- KomplexHubPlugin/downloadmanager.cpp | 819 ++++++++++++++++++ KomplexHubPlugin/downloadmanager.h | 104 +++ KomplexHubPlugin/featuredimagesmodel.cpp | 85 +- KomplexHubPlugin/featuredimagesmodel.h | 65 +- KomplexHubPlugin/featuredpacksmodel.cpp | 47 + KomplexHubPlugin/featuredpacksmodel.h | 9 + KomplexHubPlugin/featuredvideosmodel.cpp | 79 +- KomplexHubPlugin/featuredvideosmodel.h | 22 + KomplexHubPlugin/imagesearchmodel.cpp | 57 +- KomplexHubPlugin/imagesearchmodel.h | 170 ++-- KomplexHubPlugin/newestpacksmodel.cpp | 45 +- KomplexHubPlugin/newestpacksmodel.h | 13 + KomplexHubPlugin/packsearchmodel.cpp | 47 + KomplexHubPlugin/packsearchmodel.h | 9 + .../paginators/featuredpackspaginator.h | 2 +- KomplexHubPlugin/paginators/imagepaginator.h | 143 ++- KomplexHubPlugin/paginators/videopaginator.h | 22 - .../paginators/wallpaperpaginator.h | 66 +- KomplexHubPlugin/videoitemmodel.cpp | 203 +++++ KomplexHubPlugin/videoitemmodel.h | 294 +++++++ KomplexHubPlugin/videosearchmodel.cpp | 91 +- KomplexHubPlugin/videosearchmodel.h | 24 + 46 files changed, 3416 insertions(+), 578 deletions(-) create mode 100644 KomplexHubModule/Controls/PackView.qml create mode 100644 KomplexHubModule/Controls/VideoView.qml create mode 100644 KomplexHubPlugin/common/exceptions.h create mode 100644 KomplexHubPlugin/common/shaderpackmetadata.h create mode 100644 KomplexHubPlugin/common/shadertoymetadata.h create mode 100644 KomplexHubPlugin/downloadmanager.cpp create mode 100644 KomplexHubPlugin/downloadmanager.h create mode 100644 KomplexHubPlugin/videoitemmodel.cpp create mode 100644 KomplexHubPlugin/videoitemmodel.h diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index d3c1854..915697a 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -8,7 +8,8 @@ target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Quick - Qt${QT_VERSION_MAJOR}::Qml) + Qt${QT_VERSION_MAJOR}::Qml +) if(ENABLE_ASAN) message(STATUS "Building with AddressSanitizer") diff --git a/App/main.cpp b/App/main.cpp index 6e665e2..4620656 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -3,6 +3,9 @@ #include #include +#include +#include +#include #include #include #include @@ -15,7 +18,34 @@ #include #include #include +#include #include "autogen/environment.h" +#include "KomplexHubPlugin/common/komplex_global.h" + +#ifdef KOMPLEX_LOCAL_DEV +class DebugNetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory +{ +public: + QNetworkAccessManager *create(QObject *parent) override; +}; + +QNetworkAccessManager *DebugNetworkAccessManagerFactory::create(QObject *parent) +{ + QNetworkAccessManager *nam = new QNetworkAccessManager(parent); + + QObject::connect + ( + nam, + &QNetworkAccessManager::sslErrors, + nam, + [] (QNetworkReply *reply, const QList &errors) + { + reply->ignoreSslErrors(); + } + ); + return nam; +} +#endif int main(int argc, char *argv[]) { @@ -35,6 +65,10 @@ int main(int argc, char *argv[]) QCoreApplication::exit(-1); }, Qt::QueuedConnection); +#ifdef KOMPLEX_LOCAL_DEV + engine.setNetworkAccessManagerFactory(new DebugNetworkAccessManagerFactory); +#endif + engine.addImportPath(QCoreApplication::applicationDirPath() + "/qml"); engine.addImportPath(":/"); engine.load(url); diff --git a/CMakeLists.txt b/CMakeLists.txt index 5616489..8c7e887 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.21.1) option(LINK_INSIGHT "Link Qt Insight Tracker library" ON) option(BUILD_QDS_COMPONENTS "Build design studio components" ON) - + project(KomplexHub LANGUAGES CXX) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/cmake") @@ -88,6 +88,12 @@ qt_add_resources(${CMAKE_PROJECT_NAME} "app_images" "images/kero/kero_warning.png" ) + target_compile_definitions( + ${CMAKE_PROJECT_NAME} + PUBLIC + KOMPLEX_LOCAL_DEV + ) + include(qds) option(ENABLE_ASAN "Enable AddressSanitizer" OFF) @@ -138,4 +144,4 @@ install(TARGETS ${CMAKE_PROJECT_NAME} BUNDLE DESTINATION . LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} -) +) \ No newline at end of file diff --git a/KomplexHubContent/MainScreen.qml b/KomplexHubContent/MainScreen.qml index 10297e3..f610a03 100644 --- a/KomplexHubContent/MainScreen.qml +++ b/KomplexHubContent/MainScreen.qml @@ -220,9 +220,8 @@ Rectangle { } } - Rectangle { - - //Kirigami.Theme.colorSet: Kirigami.Theme.View + Rectangle + { id: windowContainer Layout.fillHeight: true @@ -414,6 +413,15 @@ Rectangle { }, State { name: "build_complete" + }, + State + { + name: "popup" + when: popup + PropertyChanges { + target: searchContainer + preferredHeight: 0 + } } ] diff --git a/KomplexHubContent/pages/HomePage.qml b/KomplexHubContent/pages/HomePage.qml index c8c194d..de5fc22 100644 --- a/KomplexHubContent/pages/HomePage.qml +++ b/KomplexHubContent/pages/HomePage.qml @@ -15,6 +15,15 @@ Item property int resultWidth: 256 property int resultHeight: 245 property bool popup: false + property bool loading: packsModel.state === NewestPacksModel.Loading || + imagesModel.state === FeaturedImagesModel.Loading || + videosModel.state === FeaturedVideosModel.Loading; + + onWidthChanged:() => { + packsModel.resultsPerPage = resultsPerRow + imagesModel.resultsPerPage = resultsPerRow + videosModel.resultsPerPage = resultsPerRow + } clip: true @@ -23,19 +32,19 @@ Item NewestPacksModel { id: packsModel - resultsPerPage: homePageRoot.resultsPerRow + resultsPerPage: 0 } FeaturedImagesModel { id: imagesModel - resultsPerPage: homePageRoot.resultsPerRow + resultsPerPage: 0 } FeaturedVideosModel { id: videosModel - resultsPerPage: homePageRoot.resultsPerRow + resultsPerPage: 0 } Rectangle @@ -46,6 +55,7 @@ Item ColumnLayout { id: resultsLayout + visible: opacity > 0.01 anchors.fill: parent anchors.margins: Constants.mediumMargin @@ -95,9 +105,14 @@ Item onTriggered: () => packsPaginator.currentIndex = parent.index - function onViewMoreTriggered() + onViewMoreTriggered: () => { + showWallpaperPopup(parent.index) + } + onThumbnailChanged: () => + { + console.log(index + " " + thumbnail + " " + parent.thumbnail) } } } @@ -196,12 +211,8 @@ Item pexels: true selected: videosPaginator.currentIndex === parent.index - onTriggered: () => packsPaginator.currentIndex = parent.index - - onViewMoreTriggered:() => - { - viewMoreImagePopup.opacity = 1 - } + onTriggered: () => videosPaginator.currentIndex = parent.index + onViewMoreTriggered:() => showVideoPopup(parent.index) } } } @@ -232,6 +243,7 @@ Item id: popupContainer anchors.fill: parent opacity: 0 + visible: opacity > 0.01 color: palette.base ImageView @@ -239,6 +251,39 @@ Item id: viewMoreImagePopup anchors.fill: parent opacity: 0 + visible: opacity > 0.01 + + Behavior on opacity { + NumberAnimation + { + duration: 250 + } + } + } + + VideoView + { + id: viewMoreVideoPopup + anchors.fill: parent + opacity: 0 + visible: opacity > 0.01 + //model: videosModel.itemModel + + Behavior on opacity { + NumberAnimation + { + duration: 250 + } + } + } + + PackView + { + id: viewMoreWallpaperPopup + anchors.fill: parent + opacity: 0 + visible: opacity > 0.01 + //model: videosModel.itemModel Behavior on opacity { NumberAnimation @@ -259,9 +304,7 @@ Item states: [ State { name: "loading" - when: packsModel.state === NewestPacksModel.Loading || - imagesModel.state === FeaturedImagesModel.Loading || - videosModel.state === FeaturedVideosModel.Loading; + when: homePageRoot.loading PropertyChanges { @@ -319,7 +362,6 @@ Item property: "opacity"; } } - ] function showImagePopup(index) @@ -328,30 +370,56 @@ Item viewMoreImagePopup.authorUrl = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.AuthorUrlRole) viewMoreImagePopup.description = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.DescriptionRole) viewMoreImagePopup.uuid = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.UuidRole) - viewMoreImagePopup.thumbnail = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.ThumbnailRole) - viewMoreImagePopup.small = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.SmallUrlRole) - viewMoreImagePopup.medium = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.MediumUrlRole) - viewMoreImagePopup.large = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.LargeUrlRole) - viewMoreImagePopup.extraLarge = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.ExtraLargeUrlRole) + viewMoreImagePopup.thumbnail = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.LargeThumbnailRole) viewMoreImagePopup.original = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.OriginalUrlRole) viewMoreImagePopup.portrait = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.PortraitUrlRole) viewMoreImagePopup.landscape = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.LandscapeUrlRole) - viewMoreImagePopup.smallSize = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.SmallSizeRole) - viewMoreImagePopup.mediumSize = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.MediumSizeRole) - viewMoreImagePopup.largeSize = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.LargeSizeRole) - viewMoreImagePopup.extraLargeSize = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.ExtraLargeSizeRole) - viewMoreImagePopup.originalSize = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.OriginalSizeRole) + viewMoreImagePopup.backgroundPortrait = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.BackgroundPortraitRole) + viewMoreImagePopup.backgroundLandscape = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.BackgroundLandscapeRole) + viewMoreImagePopup.fullScreen = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.ScreenUrlRole) viewMoreImagePopup.portraitSize = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.PortraitSizeRole) viewMoreImagePopup.landscapeSize = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.LandscapeSizeRole) + viewMoreImagePopup.fullScreenSize = imagesModel.data(imagesModel.index(index,0), FeaturedImagesModel.ScreenSizeRole) viewMoreImagePopup.opacity = 1 popupContainer.opacity = 1 + resultsLayout.opacity = 0 + homePageRoot.popup = true + } + + function showVideoPopup(index) + { + videosModel.setItem(index) + viewMoreVideoPopup.author = videosModel.data(videosModel.index(index,0), FeaturedVideosModel.AuthorRole) + viewMoreVideoPopup.authorUrl = videosModel.data(videosModel.index(index,0), FeaturedVideosModel.AuthorUrlRole) + viewMoreVideoPopup.uuid = videosModel.data(videosModel.index(index,0), FeaturedVideosModel.UuidRole) + viewMoreVideoPopup.thumbnail = videosModel.data(videosModel.index(index,0), FeaturedVideosModel.ThumbnailRole) + viewMoreVideoPopup.modelIndex = index + viewMoreVideoPopup.model = videosModel.itemModel + viewMoreVideoPopup.opacity = 1 + resultsLayout.opacity = 0 + popupContainer.opacity = 1 + homePageRoot.popup = true + } + + function showWallpaperPopup(index) + { + viewMoreWallpaperPopup.author = packsModel.data(packsModel.index(index,0), NewestPacksModel.AuthorRole) + viewMoreWallpaperPopup.authorId = packsModel.data(packsModel.index(index,0), NewestPacksModel.AuthorIdRole) + viewMoreWallpaperPopup.description = packsModel.data(packsModel.index(index,0), NewestPacksModel.DescriptionRole) + viewMoreWallpaperPopup.thumbnail = packsModel.data(packsModel.index(index,0), NewestPacksModel.ThumbnailRole) + viewMoreWallpaperPopup.opacity = 1 + popupContainer.opacity = 1 + resultsLayout.opacity = 0 homePageRoot.popup = true } function closePopup() { viewMoreImagePopup.opacity = 0 + viewMoreVideoPopup.opacity = 0 + viewMoreWallpaperPopup.opacity = 0 popupContainer.opacity = 0 + resultsLayout.opacity = 1 homePageRoot.popup = false } } diff --git a/KomplexHubContent/pages/ImageSearchPage.qml b/KomplexHubContent/pages/ImageSearchPage.qml index 53ec6b7..60656e3 100644 --- a/KomplexHubContent/pages/ImageSearchPage.qml +++ b/KomplexHubContent/pages/ImageSearchPage.qml @@ -14,6 +14,8 @@ Item property string query readonly property bool searchable: paginator.searchable property bool popup: false + property int resultWidth: 256 + property int resultHeight: 245 ImageSearchModel { @@ -33,8 +35,8 @@ Item { id: resultDelegate - width: 256 - height: 245 + width: resultWidth + height: resultHeight required property string author required property string description @@ -50,6 +52,7 @@ Item description: parent.description thumbnail: parent.thumbnail uuid: parent.uuid + pexels: true selected: paginator.currentIndex === parent.index onTriggered: () => paginator.currentIndex = parent.index @@ -93,27 +96,23 @@ Item function showImagePopup(index) { - viewMoreImagePopup.author = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.AuthorRole) - viewMoreImagePopup.authorUrl = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.AuthorUrlRole) - viewMoreImagePopup.description = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.DescriptionRole) - viewMoreImagePopup.uuid = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.UuidRole) - viewMoreImagePopup.thumbnail = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.ThumbnailRole) - viewMoreImagePopup.small = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.SmallUrlRole) - viewMoreImagePopup.medium = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.MediumUrlRole) - viewMoreImagePopup.large = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.LargeUrlRole) - viewMoreImagePopup.extraLarge = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.ExtraLargeUrlRole) - viewMoreImagePopup.original = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.OriginalUrlRole) - viewMoreImagePopup.portrait = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.PortraitUrlRole) - viewMoreImagePopup.landscape = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.LandscapeUrlRole) - viewMoreImagePopup.smallSize = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.SmallSizeRole) - viewMoreImagePopup.mediumSize = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.MediumSizeRole) - viewMoreImagePopup.largeSize = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.LargeSizeRole) - viewMoreImagePopup.extraLargeSize = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.ExtraLargeSizeRole) - viewMoreImagePopup.originalSize = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.OriginalSizeRole) - viewMoreImagePopup.portraitSize = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.PortraitSizeRole) - viewMoreImagePopup.landscapeSize = searchModel.data(searchModel.index(index,0), FeaturedImagesModel.LandscapeSizeRole) + viewMoreImagePopup.author = searchModel.data(searchModel.index(index,0), ImageSearchModel.AuthorRole) + viewMoreImagePopup.authorUrl = searchModel.data(searchModel.index(index,0), ImageSearchModel.AuthorUrlRole) + viewMoreImagePopup.description = searchModel.data(searchModel.index(index,0), ImageSearchModel.DescriptionRole) + viewMoreImagePopup.uuid = searchModel.data(searchModel.index(index,0), ImageSearchModel.UuidRole) + viewMoreImagePopup.thumbnail = searchModel.data(searchModel.index(index,0), ImageSearchModel.LargeThumbnailRole) + viewMoreImagePopup.original = searchModel.data(searchModel.index(index,0), ImageSearchModel.OriginalUrlRole) + viewMoreImagePopup.portrait = searchModel.data(searchModel.index(index,0), ImageSearchModel.PortraitUrlRole) + viewMoreImagePopup.landscape = searchModel.data(searchModel.index(index,0), ImageSearchModel.LandscapeUrlRole) + viewMoreImagePopup.backgroundPortrait = searchModel.data(searchModel.index(index,0), ImageSearchModel.BackgroundPortraitRole) + viewMoreImagePopup.backgroundLandscape = searchModel.data(searchModel.index(index,0), ImageSearchModel.BackgroundLandscapeRole) + viewMoreImagePopup.fullScreen = searchModel.data(searchModel.index(index,0), ImageSearchModel.ScreenUrlRole) + viewMoreImagePopup.portraitSize = searchModel.data(searchModel.index(index,0), ImageSearchModel.PortraitSizeRole) + viewMoreImagePopup.landscapeSize = searchModel.data(searchModel.index(index,0), ImageSearchModel.LandscapeSizeRole) + viewMoreImagePopup.fullScreenSize = searchModel.data(searchModel.index(index,0), ImageSearchModel.ScreenSizeRole) viewMoreImagePopup.opacity = 1 popupContainer.opacity = 1 + paginator.opacity = 0 rootItem.popup = true } @@ -121,6 +120,7 @@ Item { viewMoreImagePopup.opacity = 0 popupContainer.opacity = 0 + paginator.opacity = 1 rootItem.popup = false } } diff --git a/KomplexHubContent/pages/LiveSearchPage.qml b/KomplexHubContent/pages/LiveSearchPage.qml index 17a3949..cd7b129 100644 --- a/KomplexHubContent/pages/LiveSearchPage.qml +++ b/KomplexHubContent/pages/LiveSearchPage.qml @@ -11,6 +11,9 @@ Item property string query readonly property bool searchable: paginator.searchable + property bool popup: false + property int resultWidth: 256 + property int resultHeight: 245 PackSearchModel { @@ -39,6 +42,7 @@ Item required property string uuid required property string thumbnail required property string authorId + required property int index SearchResultItem { @@ -47,15 +51,66 @@ Item description: parent.description thumbnail: parent.thumbnail uuid: parent.uuid + selected: paginator.currentIndex === parent.index - selected: resultDelegate.highlighted - } + onTriggered: () => paginator.currentIndex = parent.index - MouseArea - { - anchors.fill: parent - onClicked: paginator.currentIndex = index + onViewMoreTriggered: () => + { + showWallpaperPopup(parent.index) + } } } } + + Rectangle + { + id: popupContainer + anchors.fill: parent + opacity: 0 + color: palette.base + + PackView + { + id: viewMoreWallpaperPopup + anchors.fill: parent + opacity: 0 + visible: opacity > 0.01 + //model: videosModel.itemModel + + Behavior on opacity { + NumberAnimation + { + duration: 250 + } + } + } + + Behavior on opacity { + NumberAnimation + { + duration: 250 + } + } + } + + function closePopup() + { + viewMoreWallpaperPopup.opacity = 0 + popupContainer.opacity = 0 + paginator.opacity = 1 + rootItem.popup = false + } + + function showWallpaperPopup(index) + { + viewMoreWallpaperPopup.author = searchModel.data(searchModel.index(index,0), PackSearchModel.AuthorRole) + viewMoreWallpaperPopup.authorId = searchModel.data(searchModel.index(index,0), PackSearchModel.AuthorIdRole) + viewMoreWallpaperPopup.description = searchModel.data(searchModel.index(index,0), PackSearchModel.DescriptionRole) + viewMoreWallpaperPopup.thumbnail = searchModel.data(searchModel.index(index,0), PackSearchModel.ThumbnailRole) + viewMoreWallpaperPopup.opacity = 1 + popupContainer.opacity = 1 + paginator.opacity = 0 + rootItem.popup = true + } } diff --git a/KomplexHubContent/pages/VideoSearchPage.qml b/KomplexHubContent/pages/VideoSearchPage.qml index 83ef5b7..063500b 100644 --- a/KomplexHubContent/pages/VideoSearchPage.qml +++ b/KomplexHubContent/pages/VideoSearchPage.qml @@ -13,6 +13,9 @@ Item property string query readonly property bool searchable: paginator.searchable + property bool popup: false + property int resultWidth: 256 + property int resultHeight: 245 VideoSearchModel { @@ -27,37 +30,94 @@ Item anchors.fill: parent model: searchModel loading: model.state === VideoSearchModel.Loading + visible: opacity > 0.01 delegate: ItemDelegate { id: resultDelegate - width: 256 - height: 245 + width: resultWidth + height: resultHeight - required property string author - required property string description required property string uuid - required property string thumbnail + required property string author required property string authorId + required property string authorUrl + required property string thumbnail required property int index SearchResultItem { anchors.fill: parent - title: parent.author - description: parent.description + author: parent.author + description: qsTr("No description available") thumbnail: parent.thumbnail uuid: parent.uuid + pexels: true + selected: paginator.currentIndex === parent.index - selected: resultDelegate.highlighted - } + onTriggered: () => paginator.currentIndex = parent.index - MouseArea - { - anchors.fill: parent - onClicked: paginator.currentIndex = index + onViewMoreTriggered: () => + { + showVideoPopup(parent.index) + } } } } + + Rectangle + { + id: popupContainer + anchors.fill: parent + opacity: 0 + visible: opacity > 0.01 + color: palette.base + + VideoView + { + id: viewMoreVideoPopup + anchors.fill: parent + opacity: 0 + + Behavior on opacity + { + NumberAnimation + { + duration: 250 + } + } + } + + Behavior on opacity + { + NumberAnimation + { + duration: 250 + } + } + } + + function showVideoPopup(index) + { + searchModel.setItem(index) + viewMoreVideoPopup.author = searchModel.data(searchModel.index(index,0), VideoSearchModel.AuthorRole) + viewMoreVideoPopup.authorUrl = searchModel.data(searchModel.index(index,0), VideoSearchModel.AuthorUrlRole) + viewMoreVideoPopup.uuid = searchModel.data(searchModel.index(index,0), VideoSearchModel.UuidRole) + viewMoreVideoPopup.thumbnail = searchModel.data(searchModel.index(index,0), VideoSearchModel.ThumbnailRole) + viewMoreVideoPopup.modelIndex = index + viewMoreVideoPopup.model = searchModel.itemModel + viewMoreVideoPopup.opacity = 1 + paginator.opacity = 0 + popupContainer.opacity = 1 + rootItem.popup = true + } + + function closePopup() + { + viewMoreVideoPopup.opacity = 0 + popupContainer.opacity = 0 + paginator.opacity = 1 + rootItem.popup = false + } } diff --git a/KomplexHubModule/Controls/CMakeLists.txt b/KomplexHubModule/Controls/CMakeLists.txt index 5ff7d9b..2eb4de4 100644 --- a/KomplexHubModule/Controls/CMakeLists.txt +++ b/KomplexHubModule/Controls/CMakeLists.txt @@ -18,5 +18,8 @@ qt6_add_qml_module(KomplexHubModule_Controls QML_FILES PaginatorGrid.qml QML_FILES HorizontalPaginator.qml QML_FILES ImageView.qml + QML_FILES + QML_FILES VideoView.qml + QML_FILES PackView.qml ) diff --git a/KomplexHubModule/Controls/ImageView.qml b/KomplexHubModule/Controls/ImageView.qml index 28c9ac1..dfa9ecc 100644 --- a/KomplexHubModule/Controls/ImageView.qml +++ b/KomplexHubModule/Controls/ImageView.qml @@ -17,18 +17,13 @@ Item 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 fullScreen property string portraitSize property string landscapeSize - property string smallSize - property string originalSize - property string mediumSize - property string largeSize - property string extraLargeSize + property string fullScreenSize + property string backgroundPortrait + property string backgroundLandscape property int imageHeight property int imageWidth @@ -38,7 +33,27 @@ Item Rectangle { anchors.fill: parent - color: palette.base.alpha(1) + color: palette.base + + Image + { + id: backgroundImage + source: width > height ? backgroundLandscape : backgroundPortrait + transform: Image.PreserveAspectCrop + anchors.fill: parent + visible: false + } + + MultiEffect + { + anchors.fill: backgroundImage + source: backgroundImage + blurEnabled: true + blurMax: 64 + blur: 1.0 + opacity: 0.25 + saturation: -0.8 + } ColumnLayout { @@ -50,20 +65,32 @@ Item { Layout.fillWidth: true Layout.alignment: Qt.AlignTop| Qt.AlignHCenter + Layout.topMargin: 32 - /** Spacer **/ - Item { Layout.fillWidth: true } - - Image + Rectangle { - width: imageWidth - height: imageHeight - transform: Image.PreserveAspectFit - source: rootItem.large - } + Layout.fillWidth: true + Layout.preferredHeight: width / 1.77777777778 + Layout.maximumWidth: 800 + Layout.alignment: Qt.AlignHCenter + color: palette.base.darker() - /** Spacer **/ - Item { Layout.fillWidth: true } + Throbber + { + anchors.fill: parent + id: loadingThrobber + visible: viewImage.status === Image.Loading + } + + Image + { + id: viewImage + anchors.fill: parent + transform: Image.PreserveAspectCrop + source: rootItem.thumbnail + visible: !(status === Image.Loading) + } + } } Text @@ -91,7 +118,6 @@ Item Layout.fillWidth: true } - /** Spacer **/ Item { @@ -119,12 +145,9 @@ Item { id: downloadSelector model:[ - "Small: " + smallSize, - "Medium: " + mediumSize, - "Large: " + largeSize, - "Extra Large: " + extraLargeSize, - "Portrait: " + portraitSize, - "Landscape: " + landscapeSize + "Screen (" + fullScreenSize + ")", + "Portrait (" + portraitSize + ")", + "Landscape (" + landscapeSize + ")" ] Layout.fillWidth: true Layout.preferredHeight: 36 @@ -138,21 +161,6 @@ Item } } - // Text { - // property var texts: [ - // small, - // medium, - // large, - // extraLarge, - // portrait, - // landscape - // ] - // text: texts[downloadSelector.currentIndex] - // elide: Text.ElideLeft - - // Layout.fillWidth: true - // } - /** Spacer **/ Item { Layout.fillWidth: true; Layout.fillHeight: true } diff --git a/KomplexHubModule/Controls/PackView.qml b/KomplexHubModule/Controls/PackView.qml new file mode 100644 index 0000000..c5423f0 --- /dev/null +++ b/KomplexHubModule/Controls/PackView.qml @@ -0,0 +1,177 @@ +import QtQuick +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 authorId + property string description + property string thumbnail + + property int imageHeight + property int imageWidth + + id: rootItem + + Rectangle + { + anchors.fill: parent + color: palette.base + + Image + { + id: backgroundImage + source: thumbnail + transform: Image.PreserveAspectCrop + anchors.fill: parent + visible: false + } + + MultiEffect + { + anchors.fill: backgroundImage + source: backgroundImage + blurEnabled: true + blurMax: 64 + blur: 1.0 + opacity: 0.25 + saturation: -0.8 + } + + ColumnLayout + { + anchors.fill: parent + anchors.margins: Constants.largeMargin + spacing: Constants.largeMargin + + RowLayout + { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop| Qt.AlignHCenter + Layout.topMargin: 32 + + Rectangle + { + Layout.fillWidth: true + Layout.preferredHeight: width / 1.77777777778 + Layout.maximumWidth: 800 + Layout.alignment: Qt.AlignHCenter + color: palette.base.darker() + + Throbber + { + anchors.fill: parent + id: loadingThrobber + visible: viewImage.status === Image.Loading + } + + Image + { + id: viewImage + anchors.fill: parent + transform: Image.PreserveAspectCrop + source: rootItem.thumbnail + visible: !(status === Image.Loading) + } + } + } + + Text + { + id: titleText + text: rootItem.author + color: palette.text + font.bold: true + font.pixelSize: Constants.h3Font.pixelSize + elide: Text.ElideRight + + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop| Qt.AlignLeft + } + + Text + { + id: descriptionText + text: rootItem.description + color: palette.text + font.pointSize: Constants.h4Font.pixelSize + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + + Layout.alignment: Qt.AlignTop| Qt.AlignLeft + Layout.fillWidth: true + } + + /** Spacer **/ + Item + { + Layout.fillWidth: true + Layout.preferredHeight: 32 + } + + // Text + // { + // text: qsTr("Available Downloads") + // color: palette.text + // font.bold: true + // font.pixelSize: Constants.h3Font.pixelSize + // elide: Text.ElideRight + + // Layout.fillWidth: true + // Layout.alignment: Qt.AlignTop| Qt.AlignLeft + // } + + RowLayout + { + Layout.fillWidth: true + + Item { Layout.fillWidth: true } + + SquareButton + { + Layout.preferredHeight: 36 + Layout.preferredWidth: 128 + text: qsTr("Download") + icon.source: "qrc:/images/icons/icons8-download.svg" + } + } + + /** Spacer **/ + Item { Layout.fillWidth: true; Layout.fillHeight: true } + + RowLayout + { + Layout.fillWidth: true + Layout.alignment: Qt.AlignBottom + + Text { + text: qsTr("Shaders Provided Courtesy of ShaderToy and Their Respective Authors") + elide: Text.ElideLeft + color: palette.text + font.bold: true + font.pixelSize: Constants.h6Font.pixelSize + + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + } + + Image + { + transform: Image.PreserveAspectFit + source: "qrc:/images/icons/pexels-icon-filled-256.svg" + + Layout.alignment: Qt.AlignRight + Layout.preferredHeight: 32 + Layout.preferredWidth: 32 + } + } + } + } +} diff --git a/KomplexHubModule/Controls/SearchResultItem.qml b/KomplexHubModule/Controls/SearchResultItem.qml index 60c7442..d22d34b 100644 --- a/KomplexHubModule/Controls/SearchResultItem.qml +++ b/KomplexHubModule/Controls/SearchResultItem.qml @@ -16,7 +16,7 @@ Item { signal triggered signal viewMoreTriggered - id: searchResultItemRoot + id: mainItem width: 256 height: 256 clip: true @@ -48,7 +48,7 @@ Item { onReleased: () => { if(!states.selected) { - searchResultItemRoot.triggered() + mainItem.triggered() } } } @@ -68,12 +68,35 @@ Item { color: palette.base.lighter(1.25) - Image + Throbber { anchors.fill: parent + id: loadingThrobber + visible: viewImage.status === Image.Loading + } + + Image + { + property int retries: 0 + id: viewImage + anchors.fill: parent + source: mainItem.thumbnail + visible: !(status === Image.Loading) fillMode: Image.PreserveAspectCrop - id: thumnailImage - source: searchResultItemRoot.thumbnail + + onStatusChanged: () => + { + // if(status === Image.Error) + // { + // if(retries <= 3) + // { + // ++retries + // let src = source + // source = "" + // source = src + // } + // } + } } Image @@ -89,14 +112,14 @@ Item { opacity: 0.5 source: "qrc:/images/icons/pexels-icon-filled-256.svg" - visible: searchResultItemRoot.pexels + visible: mainItem.pexels } } Text { color: palette.text - text: searchResultItemRoot.title + text: mainItem.title font.pixelSize: 16 font.bold: true @@ -112,12 +135,12 @@ Item { { color: palette.text font.pixelSize: 12 - text: "By: " + searchResultItemRoot.author + text: "By: " + mainItem.author leftPadding: 12 Layout.alignment: Qt.AlignLeft | Qt.AlignBottom Layout.preferredWidth: 250 - visible: !searchResultItemRoot.pexels + visible: !mainItem.pexels } Text @@ -126,7 +149,7 @@ Item { clip: true color: palette.text font.pixelSize: 12 - text: searchResultItemRoot.description + text: mainItem.description wrapMode: Text.WrapAtWordBoundaryOrAnywhere textFormat: Text.PlainText elide: Text.ElideRight @@ -153,7 +176,7 @@ Item { opacity: 0 onTriggered: () => { - searchResultItemRoot.viewMoreTriggered() + mainItem.viewMoreTriggered() } } @@ -166,7 +189,7 @@ Item { id: states property bool clicked: false property bool hovered: false - property alias selected: searchResultItemRoot.selected + property alias selected: mainItem.selected property bool idle: true onIdleChanged: () => diff --git a/KomplexHubModule/Controls/SquareButton.qml b/KomplexHubModule/Controls/SquareButton.qml index e5236a9..a9f53f7 100644 --- a/KomplexHubModule/Controls/SquareButton.qml +++ b/KomplexHubModule/Controls/SquareButton.qml @@ -25,7 +25,7 @@ Item { component IconSettings: QtObject { property int width: 16 property int height: 16 - property string source: "images/icons/icons8-next.svg" + property string source: "qrc:/images/icons/icons8-next.svg" } component BorderSettings: QtObject { diff --git a/KomplexHubModule/Controls/Throbber.qml b/KomplexHubModule/Controls/Throbber.qml index 7973eab..fc4fdfa 100644 --- a/KomplexHubModule/Controls/Throbber.qml +++ b/KomplexHubModule/Controls/Throbber.qml @@ -1,126 +1,159 @@ import QtQuick import QtQuick.Shapes import QtQuick.Effects +import QtQuick.Layouts Item { id: throbberRoot - Rectangle { - anchors.fill: parent - color: "transparent" - - Item + onHeightChanged: () => { + if(height > width) { - id: outerRing - anchors.fill: parent - - Shape - { - anchors.fill: parent - asynchronous: true - - ShapePath - { - property double progress: 0 - property int dashSpaceLength: 15 - property int dashLength: 1 - - id: outerRingPath - strokeColor: palette.accent.darker(1 + (progress - 0.25)) - fillColor: "transparent" - strokeWidth: 3 - strokeStyle: ShapePath.DashLine - - startX: 0 - startY: outerRing.height * 0.5 - - dashPattern: [ - dashSpaceLength - (dashSpaceLength * progress), - dashSpaceLength * progress - ] - - PathArc - { - x: outerRing.width - y: outerRing.height * 0.5 - - radiusX: outerRing.width / 2 - radiusY: outerRing.height / 2 - - useLargeArc: false - } - - PathArc - { - x: 0 - y: outerRing.height * 0.5 - - radiusX: outerRing.width / 2 - radiusY: outerRing.height / 2 - - useLargeArc: false - } - } - } - - transform: Rotation { - id: outerRingRotation - origin.x: outerRing.width / 2 - origin.y: outerRing.height / 2 - } - - NumberAnimation { - property bool reverse: false - - target: outerRingPath - property: "progress" - duration: 1600 - //easing.type: Easing.InOutQuad - - from: 0 - to: 1 - loops: 1 - running: true - - onFinished: () => { - if(reverse) - { - from = 0 - to = 1 - start() - } - else - { - from = 1 - to = 0 - start() - } - - reverse = !reverse - } - } - - NumberAnimation { - target: outerRingRotation - property: "angle" - duration: 16000 - easing.type: Easing.InOutQuad - - from: 0 - to: (360 * 4) - loops: NumberAnimation.Infinite - running: true - } + layout.throbberSize = width - 24 } + else + { + layout.throbberSize = height - 24 + } + } - MultiEffect { - source: outerRing - anchors.fill: outerRing - brightness: 0.4 - saturation: 0.2 - blurEnabled: true - blurMax: 64 - blur: 1.0 + onWidthChanged: () => { + if(height > width) + { + layout.throbberSize = width - 24 + } + else + { + layout.throbberSize = height - 24 + } + } + + RowLayout + { + property int throbberSize + id: layout + anchors.fill: parent + Rectangle + { + Layout.alignment: Qt.AlignHCenter + Layout.preferredHeight: parent.throbberSize + Layout.preferredWidth: parent.throbberSize + + color: "transparent" + + Item + { + id: outerRing + anchors.fill: parent + + Shape + { + anchors.fill: parent + asynchronous: true + + ShapePath + { + property double progress: 0 + property int dashSpaceLength: 15 + property int dashLength: 1 + + id: outerRingPath + strokeColor: palette.accent.darker(1 + (progress - 0.25)) + fillColor: "transparent" + strokeWidth: 3 + strokeStyle: ShapePath.DashLine + + startX: 0 + startY: outerRing.height * 0.5 + + dashPattern: [ + dashSpaceLength - (dashSpaceLength * progress), + dashSpaceLength * progress + ] + + PathArc + { + x: outerRing.width + y: outerRing.height * 0.5 + + radiusX: outerRing.width / 2 + radiusY: outerRing.height / 2 + + useLargeArc: false + } + + PathArc + { + x: 0 + y: outerRing.height * 0.5 + + radiusX: outerRing.width / 2 + radiusY: outerRing.height / 2 + + useLargeArc: false + } + } + } + + transform: Rotation { + id: outerRingRotation + origin.x: outerRing.width / 2 + origin.y: outerRing.height / 2 + } + + NumberAnimation { + property bool reverse: false + + target: outerRingPath + property: "progress" + duration: 1600 + //easing.type: Easing.InOutQuad + + from: 0 + to: 1 + loops: 1 + running: true + + onFinished: () => { + if(reverse) + { + from = 0 + to = 1 + start() + } + else + { + from = 1 + to = 0 + start() + } + + reverse = !reverse + } + } + + NumberAnimation { + target: outerRingRotation + property: "angle" + duration: 16000 + easing.type: Easing.InOutQuad + + from: 0 + to: (360 * 4) + loops: NumberAnimation.Infinite + running: true + } + } + + MultiEffect { + source: outerRing + anchors.fill: outerRing + brightness: 0.4 + saturation: 0.2 + blurEnabled: true + blurMax: 64 + blur: 1.0 + } } } } diff --git a/KomplexHubModule/Controls/VideoView.qml b/KomplexHubModule/Controls/VideoView.qml new file mode 100644 index 0000000..fbe48c3 --- /dev/null +++ b/KomplexHubModule/Controls/VideoView.qml @@ -0,0 +1,191 @@ +import QtQuick +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 thumbnail + property alias model: downloadSelector.model + property int modelIndex + + property int imageHeight + property int imageWidth + + visible: opacity > 0.01 + onVisibleChanged: () => + { + if(!visible) + { + model = null + } + } + + Connections + { + target: model + + function onRowCountChanged() + { + downloadSelector.currentIndex = 0 + } + } + + id: rootItem + + Rectangle + { + anchors.fill: parent + color: palette.base + + Image + { + id: backgroundImage + source: thumbnail + anchors.fill: parent + visible: false + fillMode: Image.PreserveAspectFit + } + + MultiEffect + { + anchors.fill: backgroundImage + source: backgroundImage + blurEnabled: true + blurMax: 64 + blur: 1.0 + opacity: 0.25 + saturation: -0.8 + } + + ColumnLayout + { + anchors.fill: parent + anchors.margins: Constants.largeMargin + spacing: Constants.largeMargin + + RowLayout + { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop| Qt.AlignHCenter + Layout.topMargin: 32 + + Rectangle + { + Layout.fillWidth: true + Layout.preferredHeight: width / 1.77777777778 + Layout.maximumWidth: 800 + Layout.alignment: Qt.AlignHCenter + color: palette.base.darker() + + Throbber + { + anchors.fill: parent + id: loadingThrobber + visible: viewImage.status === Image.Loading + } + + Image + { + id: viewImage + anchors.fill: parent + source: rootItem.thumbnail + visible: !(status === Image.Loading) + fillMode: Image.PreserveAspectCrop + } + } + } + + Text + { + id: titleText + text: rootItem.author + color: palette.text + font.bold: true + font.pixelSize: Constants.h3Font.pixelSize + elide: Text.ElideRight + + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop| Qt.AlignLeft + } + + /** Spacer **/ + Item + { + Layout.fillWidth: true + Layout.preferredHeight: 32 + } + + Text + { + text: qsTr("Available Downloads") + color: palette.text + font.bold: true + font.pixelSize: Constants.h3Font.pixelSize + elide: Text.ElideRight + + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop| Qt.AlignLeft + } + + RowLayout + { + Layout.fillWidth: true + + ComboBox + { + id: downloadSelector + textRole: "text" + valueRole: "url" + Layout.fillWidth: true + Layout.preferredHeight: 36 + } + SquareButton + { + Layout.preferredHeight: 36 + Layout.preferredWidth: 128 + text: qsTr("Download") + icon.source: "qrc:/images/icons/icons8-download.svg" + } + } + + /** Spacer **/ + Item { Layout.fillWidth: true; Layout.fillHeight: true } + + RowLayout + { + Layout.fillWidth: true + Layout.alignment: Qt.AlignBottom + + Text { + text: qsTr("Videos Provided Courtesy of Pexels and Their Respective Authors") + elide: Text.ElideLeft + color: palette.text + font.bold: true + font.pixelSize: Constants.h6Font.pixelSize + + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + } + + Image + { + transform: Image.PreserveAspectFit + source: "qrc:/images/icons/pexels-icon-filled-256.svg" + + Layout.alignment: Qt.AlignRight + Layout.preferredHeight: 32 + Layout.preferredWidth: 32 + } + } + } + } +} diff --git a/KomplexHubPlugin/CMakeLists.txt b/KomplexHubPlugin/CMakeLists.txt index 2002866..35b7a6f 100644 --- a/KomplexHubPlugin/CMakeLists.txt +++ b/KomplexHubPlugin/CMakeLists.txt @@ -25,45 +25,52 @@ qt_add_qml_module( common/logging.h common/fetchresult.h paginators/newestpackspaginator.h - SOURCES paginators/featuredpackspaginator.h - SOURCES paginators/featuredimagespaginator.h - SOURCES paginators/wallpaperpaginator.h - SOURCES paginators/packsearchpaginator.h - SOURCES paginators/imagesearchpaginator.h - SOURCES paginators/newestshaderspaginator.h - SOURCES paginators/newestcubemapspaginator.h - SOURCES paginators/featuredshaderspaginator.h - SOURCES paginators/featuredcubemapspaginator.h - SOURCES paginators/searchshaderspaginator.h - SOURCES paginators/searchcubemapspaginator.h - SOURCES paginators/videosearchpaginator.h - SOURCES featuredpacksmodel.h - SOURCES featuredpacksmodel.cpp - SOURCES featuredimagesmodel.h - SOURCES featuredimagesmodel.cpp - SOURCES featuredvideosmodel.h - SOURCES featuredvideosmodel.cpp - SOURCES paginators/featuredvideospaginator.h - SOURCES common/imagecache.h - SOURCES paginators/imagepaginator.h - SOURCES common/videocache.h - SOURCES paginators/videopaginator.h - SOURCES imagesearchmodel.cpp - SOURCES imagesearchmodel.h - SOURCES videosearchmodel.h - SOURCES videosearchmodel.cpp - SOURCES - SOURCES - SOURCES packsearchmodel.cpp - SOURCES packsearchmodel.h - + paginators/featuredpackspaginator.h + paginators/featuredimagespaginator.h + paginators/wallpaperpaginator.h + paginators/packsearchpaginator.h + paginators/imagesearchpaginator.h + paginators/newestshaderspaginator.h + paginators/newestcubemapspaginator.h + paginators/featuredshaderspaginator.h + paginators/featuredcubemapspaginator.h + paginators/searchshaderspaginator.h + paginators/searchcubemapspaginator.h + paginators/videosearchpaginator.h + featuredpacksmodel.h + featuredpacksmodel.cpp + featuredimagesmodel.h + featuredimagesmodel.cpp + featuredvideosmodel.h + featuredvideosmodel.cpp + paginators/featuredvideospaginator.h + common/imagecache.h + paginators/imagepaginator.h + common/videocache.h + paginators/videopaginator.h + imagesearchmodel.cpp + imagesearchmodel.h + videosearchmodel.h + videosearchmodel.cpp + packsearchmodel.cpp + packsearchmodel.h + videoitemmodel.h + videoitemmodel.cpp + downloadmanager.h downloadmanager.cpp + common/exceptions.h + common/shadertoymetadata.h + common/shaderpackmetadata.h ) target_compile_definitions( KomplexHubPlugin PUBLIC KOMPLEX_PLUGIN - KOMPLEX_LOCAL_DEV +) + +target_link_libraries(KomplexHubPlugin PRIVATE + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui ) if(ENABLE_ASAN) diff --git a/KomplexHubPlugin/common/coreservices.cpp b/KomplexHubPlugin/common/coreservices.cpp index 4ddb998..4559ccc 100644 --- a/KomplexHubPlugin/common/coreservices.cpp +++ b/KomplexHubPlugin/common/coreservices.cpp @@ -50,3 +50,15 @@ auto CoreServices::sessionToken() -> QByteArray {} ); } + +auto CoreServices::screenSize() -> QSize +{ + QScreen *screen = QGuiApplication::primaryScreen(); + + if(screen == nullptr) + { + return QSize(0,0); + } + + return screen->size(); +} diff --git a/KomplexHubPlugin/common/coreservices.h b/KomplexHubPlugin/common/coreservices.h index 63f8903..7f2fd1c 100644 --- a/KomplexHubPlugin/common/coreservices.h +++ b/KomplexHubPlugin/common/coreservices.h @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -45,6 +46,8 @@ public: */ static auto sessionToken() -> QByteArray; + static auto screenSize() -> QSize; + private: CoreServices() = default; diff --git a/KomplexHubPlugin/common/exceptions.h b/KomplexHubPlugin/common/exceptions.h new file mode 100644 index 0000000..811838c --- /dev/null +++ b/KomplexHubPlugin/common/exceptions.h @@ -0,0 +1,28 @@ +#ifndef EXCEPTIONS_H +#define EXCEPTIONS_H +#include +#include +#include "komplex_global.h" + +struct KOMPLEX_EXPORT Exception : std::exception +{ + explicit Exception(const QString &message, quint16 errorCode = 0) + : std::exception(), message(message), errorCode(errorCode){} + + const QString message; + const quint16 errorCode; +}; + +struct NetworkException : Exception +{ + NetworkException(const QString &message, qsizetype errorCode = 0) : Exception(message, errorCode) {} +}; +struct FileException : Exception +{ + FileException(const QString &message, qsizetype errorCode = 0) : Exception(message, errorCode) {} +}; +struct SqlException : Exception +{ + SqlException(const QString &message, qsizetype errorCode = 0) : Exception(message, errorCode) {} +}; +#endif // EXCEPTIONS_H diff --git a/KomplexHubPlugin/common/komplex_global.h b/KomplexHubPlugin/common/komplex_global.h index a0f1a57..34f51d3 100644 --- a/KomplexHubPlugin/common/komplex_global.h +++ b/KomplexHubPlugin/common/komplex_global.h @@ -31,11 +31,12 @@ #define SLIDING_CACHE_PREFETCH_THRESHOLD 20 #define NETWORK_TIMEOUT 10000 +#define NETWORK_RETRY_LIMIT 3 #ifdef KOMPLEX_LOCAL_DEV #define KOMPLEX_API_HOST "https://komplexapi.hlab" #else -#define KOMPLEX_API_HOST "https://komplex.digitalartifex.dev" +#define KOMPLEX_API_HOST "https://api.komplex.dev" #endif #define KOMPLEX_API_VERSION "v2" diff --git a/KomplexHubPlugin/common/paginator.h b/KomplexHubPlugin/common/paginator.h index b6669a0..01acaa7 100644 --- a/KomplexHubPlugin/common/paginator.h +++ b/KomplexHubPlugin/common/paginator.h @@ -23,6 +23,9 @@ #include #include #include +#include +#include +#include #include "komplex_global.h" #include "slidingcachecontroller.h" @@ -47,6 +50,7 @@ signals: auto pageChanged() -> void; auto countChanged() -> void; auto queryChanged() -> void; + auto uriChanged() -> void; }; /** @@ -168,6 +172,34 @@ public: setOffset(0); } + /** + * @brief uri + * URI of the API Endpoint. This is set by the child class + * @return + */ + [[nodiscard]] + auto uri() const -> QString + { + return m_uri; + } + + /** + * @brief setUri + * Sets the API Endpoint URI to fetch from + * @param uri + */ + auto setUri(const QString &uri) -> void + { + if(uri == m_uri) + { + return; + } + + m_uri = uri; + + Q_EMIT uriChanged(); + } + /** * @brief data * @return @@ -321,6 +353,31 @@ protected: controller()->reset(); } + /** + * @brief getDocument + * Extracts the JSON document from the server reply and deletes the reply + * object. + * @param reply + * @return + */ + [[nodiscard]] + auto getDocument(QNetworkReply *reply) const -> QJsonDocument + { + QByteArray data = reply->readAll(); + QJsonParseError documentError; + QJsonDocument document = QJsonDocument::fromJson(data, &documentError); + + reply->deleteLater(); + + LOG_ERROR_X(documentError.error != QJsonParseError::NoError, + "NewestPacksPaginator::getDocument", + documentError.errorString().toStdString().c_str(), + {} + ); + + return document; + } + private: /** @@ -377,6 +434,12 @@ private: * Controls the queryable state of the paginator */ bool m_queryable = false; + + /** + * @brief m_uri + * API Endpoint URI + */ + QString m_uri; }; #endif // PAGINATOR_H diff --git a/KomplexHubPlugin/common/shaderpackmetadata.h b/KomplexHubPlugin/common/shaderpackmetadata.h new file mode 100644 index 0000000..e51ce67 --- /dev/null +++ b/KomplexHubPlugin/common/shaderpackmetadata.h @@ -0,0 +1,138 @@ +#ifndef SHADERPACKMETADATA +#define SHADERPACKMETADATA + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "komplex_global.h" + +class KOMPLEX_EXPORT ShaderPackMetadata : public QObject +{ + Q_OBJECT + QML_ELEMENT + + QString m_author; + QString m_description; + QString m_engine; + QString m_file; + QString m_id; + QString m_license; + QString m_name; + QString m_version; + + Q_PROPERTY(QString author READ author WRITE setAuthor NOTIFY authorChanged) + Q_PROPERTY(QString description READ description WRITE setDescription NOTIFY descriptionChanged) + Q_PROPERTY(QString engine READ engine WRITE setEngine NOTIFY engineChanged) + Q_PROPERTY(QString file READ file WRITE setFile NOTIFY fileChanged) + Q_PROPERTY(QString id READ id WRITE setId NOTIFY idChanged) + Q_PROPERTY(QString license READ license WRITE setLicense NOTIFY licenseChanged) + Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) + Q_PROPERTY(QString version READ version WRITE setVersion NOTIFY versionChanged) + +Q_SIGNALS: + void authorChanged(); + void descriptionChanged(); + void engineChanged(); + void idChanged(); + void licenseChanged(); + void nameChanged(); + void versionChanged(); + void fileChanged(); + +public: + + QString author() const { return m_author; } + void setAuthor(const QString& author) + { + if(author != m_author) + { + m_author = author; + Q_EMIT authorChanged(); + } + } + + QString description() const { return m_description; } + void setDescription(const QString& description) + { + if(description != m_description) + { + m_description = description; + Q_EMIT descriptionChanged(); + } + } + + QString engine() const { return m_engine; } + void setEngine(const QString& engine) + { + if(engine != m_engine) + { + m_engine = engine; + Q_EMIT engineChanged(); + } + } + + QString file() const { return m_file; } + void setFile(const QString& file) + { + if(file != m_file) + { + m_file = file; + Q_EMIT fileChanged(); + } + } + + QString id() const { return m_id; } + void setId(const QString& id) + { + if(id != m_id) + { + m_id = id; + Q_EMIT idChanged(); + } + } + + QString license() const { return m_license; } + void setLicense(const QString& license) + { + if(license != m_license) + { + m_license = license; + Q_EMIT licenseChanged(); + } + } + + QString name() const { return m_name; } + void setName(const QString& name) + { + if(name != m_name) + { + m_name = name; + Q_EMIT nameChanged(); + } + } + + QString version() const { return m_version; } + void setVersion(const QString& version) + { + if(version != m_version) + { + m_version = version; + Q_EMIT versionChanged(); + } + } +}; + +Q_DECLARE_METATYPE(ShaderPackMetadata) +#endif \ No newline at end of file diff --git a/KomplexHubPlugin/common/shadertoymetadata.h b/KomplexHubPlugin/common/shadertoymetadata.h new file mode 100644 index 0000000..66e6641 --- /dev/null +++ b/KomplexHubPlugin/common/shadertoymetadata.h @@ -0,0 +1,108 @@ +#ifndef SHADERTOYMETADATA_H +#define SHADERTOYMETADATA_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "komplex_global.h" + +struct KOMPLEX_EXPORT ShaderToyMetadata +{ + QDateTime date; + QString description; + quint64 flags = 0; + bool hasLiked = false; + QString id; + quint64 likes = 0; + QString name; + quint64 published = 0; + QStringList tags; + bool usePreview = false; + QString username; + QString version; + quint64 views = 0; +}; + +struct KOMPLEX_EXPORT ShaderToyRenderInput +{ + quint8 channel = 0; + QString ctype; + QString filter; + quint64 id = 0; + QString internal; + bool published = false; + QString source; + bool srgb = false; + bool verticalFlip = false; + QString wrap; +}; + +struct KOMPLEX_EXPORT ShaderToyRenderOutput +{ + const quint8 channel = 0; + const quint64 id = 0; +}; + +struct KOMPLEX_EXPORT ShaderToyRenderPass +{ + QByteArray code; + QString description; + QList inputs; + QString name; + QList outputs; + QString type; +}; + +enum KOMPLEX_EXPORT ShaderToyErrorCode +{ + NoError = 0, + NetworkError = (0x10 << 24), + MediaError = (0x20 << 24), + DiskError = (0x30 << 24), + CompileError = (0x40 << 24) +}; + +struct KOMPLEX_EXPORT ShaderToyError +{ + QString message; + ShaderToyErrorCode code; +}; + +struct KOMPLEX_EXPORT ShaderToyEntry +{ + enum Status + { + Idle, + Loading, + Compiling, + Compiled, + Error + }; + + ShaderToyMetadata metadata; + QList renderPasses; + QByteArray data; + ShaderToyError error; + QStringList videoSelections; + Status status = Idle; +}; + +Q_DECLARE_METATYPE(ShaderToyEntry) +Q_DECLARE_METATYPE(ShaderToyError) +#endif // SHADERTOYMETADATA_H diff --git a/KomplexHubPlugin/common/slidingcachecontroller.h b/KomplexHubPlugin/common/slidingcachecontroller.h index 91846a1..68541e1 100644 --- a/KomplexHubPlugin/common/slidingcachecontroller.h +++ b/KomplexHubPlugin/common/slidingcachecontroller.h @@ -96,7 +96,6 @@ public: auto at(qsizetype index) const -> const T& { QMutexLocker cacheLocker(&m_cacheMutex); - T defaultValue; if(!externalBoundaryCheck(index)) { @@ -203,7 +202,7 @@ public: return; } - if(!externalBoundaryCheck(index + count)) + if(externalBoundaryCheck(index + count)) { slideWindow(index); } @@ -589,6 +588,8 @@ private: * Override for window size */ qsizetype m_windowSize = nullsize; + + T defaultValue; }; #endif // SLIDINGCACHECONTROLLER_H diff --git a/KomplexHubPlugin/downloadmanager.cpp b/KomplexHubPlugin/downloadmanager.cpp new file mode 100644 index 0000000..e9ec143 --- /dev/null +++ b/KomplexHubPlugin/downloadmanager.cpp @@ -0,0 +1,819 @@ +#include "downloadmanager.h" +#include "common/coreservices.h" +#include "common/exceptions.h" +#include "common/logging.h" +#include "common/shaderpackmetadata.h" + +DownloadManager::DownloadManager(QObject *parent) + : QObject{parent} +{ + +} + +auto DownloadManager::downloadImage(const QString &author, const QString &authorId, const QString &description, const QUrl &url) noexcept(false) -> void +{ + QFuture future = QtConcurrent::run + ( + [this, url, author, authorId, description]() + { + QNetworkRequest request(url); + QUrl localUri; + QString id = QUuid::createUuidV7().toString(QUuid::WithoutBraces); + + try + { + QUrl downloadUri = download(request, id); + + ShaderPackMetadata metadata; + metadata.setAuthor(author); + metadata.setDescription(description); + metadata.setName(QStringLiteral("Pexels Image (%1)").arg(id)); + + setState(Complete); + Q_EMIT downloadComplete(downloadUri.toString()); + } + catch (FileException e) + { + setError(QStringLiteral("File Exception"), e.message); + localUri.clear(); + } + catch (NetworkException e) + { + setError(QStringLiteral("Network Exception"), e.message); + localUri.clear(); + } + } + ); +} + +auto DownloadManager::downloadPack(const QString &id) noexcept(false) -> void +{ + QFuture future = QtConcurrent::run + ( + [this, id]() + { + QUrl uri = QUrl + ( + QStringLiteral("%1/%2/%3").arg + ( + KOMPLEX_API_HOST, + KOMPLEX_API_VERSION, + KOMPLEX_ENDPOINT_PACKS_ITEM, + id + ) + ); + + QNetworkRequest request(uri); + request.setRawHeader(QByteArray("uuid"), id.toUtf8()); + + QUrl localUri; + + try + { + QUrl tempUri = download(request, id); + QUrl decompressedUri = decompress(tempUri); + + ShaderToyEntry entry = readShaderToyEntry(decompressedUri); + //scan and replace media via user choice. resave with decompressedUri + + QUrl compiledUri = compile(decompressedUri); + QUrl installedUri = install(compiledUri); + + setState(Complete); + + Q_EMIT downloadComplete(installedUri.toString()); + } + catch (FileException e) + { + setError(QStringLiteral("File Exception"), e.message); + localUri.clear(); + } + catch (NetworkException e) + { + setError(QStringLiteral("Network Exception"), e.message); + localUri.clear(); + } + } + ); +} + +auto DownloadManager::reset() -> void +{ + setError(QString(), QString()); + setState(Idle); +} + +auto DownloadManager::setError(const QString &title, const QString &message) -> void +{ + if(title == m_errorTitle && message == m_errorMessage) + { + return; + } + + m_errorMessage = std::move(message); + m_errorTitle = std::move(title); + Q_EMIT errorChanged(); +} + +auto DownloadManager::compile(const QUrl &uri) noexcept(false) -> QUrl +{ + if(!uri.isLocalFile()) + { + throw FileException(QStringLiteral("Uri needs to be a local file"), 0); + } + + setState(Compiling); + + QDir localToolsDirectory + ( + QStringLiteral("%1/.local/share/komplex/tools").arg + ( + QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + ) + ); + + QDir outputDirectory = QStringLiteral("%1/komplex/build").arg + ( + QStandardPaths::writableLocation(QStandardPaths::TempLocation) + ); + + QStringList arguments = + { + localToolsDirectory.absoluteFilePath(QStringLiteral("stc.py")), + QStringLiteral("-i"), + uri.toLocalFile(), + QStringLiteral("-o"), + outputDirectory.absolutePath() + }; + + if(!QFile::exists(localToolsDirectory.absoluteFilePath(QStringLiteral("stc.py")))) + { + throw FileException + ( + QStringLiteral("Shader Compiler is not installed at %1").arg + ( + localToolsDirectory.absoluteFilePath(QStringLiteral("stc.py")) + ) + ); + } + + QProcess *process = new QProcess(this); + + QObject::connect + ( + process, + &QProcess::readyReadStandardOutput, + this, + [this, process]() + { + QByteArray processData = process->readAllStandardOutput(); + setCompilerOutput(m_compilerOutput + processData); + } + ); + + QObject::connect + ( + process, + &QProcess::readyReadStandardError, + this, + [this, process]() + { + QByteArray processData = process->readAllStandardError(); + + if(!processData.isValidUtf8()) + { + qWarning() << QStringLiteral("Process output not valid UTF8 data"); + return; + } + + setCompilerOutput(m_compilerOutput + processData); + } + ); + + process->start(QStringLiteral("python3"), arguments); + + if(!process->waitForStarted(3000)) + { + process->deleteLater(); + throw FileException(QStringLiteral("Could not start shader compiler")); + } + + if(!process->waitForFinished()) + { + process->deleteLater(); + throw FileException(QStringLiteral("Shader compiler timeout")); + } + + if(process->exitCode() != 0) + { + process->deleteLater(); + throw FileException(QStringLiteral("Shader compiler error")); + } + + process->deleteLater(); + + QUrl outputUri = outputDirectory.absolutePath(); + outputUri.setScheme(QStringLiteral("file://")); + + return outputUri; +} + +void DownloadManager::setCompilerOutput(const QString &compilerOutput) +{ + if (m_compilerOutput == compilerOutput) + { + return; + } + + m_compilerOutput = compilerOutput; + emit compilerOutputChanged(); +} + +auto DownloadManager::setState(State state) -> void +{ + if(state == m_state) + { + return; + } + + m_state = state; + Q_EMIT stateChanged(); + + return; +} + +auto DownloadManager::setDownloadProgress(qreal progress) -> void +{ + if(qFuzzyCompare(progress, m_downloadProgress)) + { + return; + } + + m_downloadProgress = progress; + Q_EMIT downloadProgressChanged(); +} + +auto DownloadManager::decompress(const QUrl &uri) noexcept(false) -> QUrl +{ + return {}; +} + +// void DownloadManager::save(ShaderToyEntry entry) +// { +// QString directoryLocation = QStringLiteral("%1/komplex/src/%2").arg(QStandardPaths::writableLocation(QStandardPaths::TempLocation), entry.metadata.id); +// QDir directory(directoryLocation); + +// if(!directory.exists()) +// { +// directory.mkpath(directoryLocation + QStringLiteral("/shaders")); +// directory.mkpath(directoryLocation + QStringLiteral("/images")); +// directory.mkpath(directoryLocation + QStringLiteral("/videos")); +// } + +// QDir shaderDirectory(directoryLocation + QStringLiteral("/shaders")); +// QDir imageDirectory(directoryLocation + QStringLiteral("/images")); +// // QDir videoDirectory(directoryLocation + QStringLiteral("/videos")); + +// QJsonObject rootObject; +// rootObject[QStringLiteral("author")] = entry.metadata.username; +// rootObject[QStringLiteral("name")] = entry.metadata.name; +// rootObject[QStringLiteral("version")] = entry.metadata.version; +// rootObject[QStringLiteral("engine")] = QStringLiteral("shadertoy"); +// rootObject[QStringLiteral("description")] = entry.metadata.description; +// rootObject[QStringLiteral("id")] = entry.metadata.id; +// rootObject[QStringLiteral("tags")] = QJsonArray::fromStringList(entry.metadata.tags); +// QMap externalMedia; + +// externalMedia.insert +// ( +// directory.absoluteFilePath(QStringLiteral("thumbnail.jpg")), +// QStringLiteral("/media/shaders/%1.jpg").arg(entry.metadata.id) +// ); + +// for(const ShaderToyRenderPass &pass : std::as_const(entry.renderPasses)) +// { +// // skip tone generators +// if(pass.type == QStringLiteral("sound")) +// continue; + +// QString passName = pass.name; + +// if(passName.contains(QStringLiteral("Buf")) && !passName.contains(QStringLiteral("Buffer"))) +// passName.replace(QStringLiteral("Buf"), QStringLiteral("Buffer")); + +// QFile shaderFile(shaderDirectory.absoluteFilePath(passName + QStringLiteral(".frag"))); + +// if(!shaderFile.open(QFile::WriteOnly)) +// { +// qWarning() << QStringLiteral("Could not open shader file for saving"); +// return; +// } + +// if(shaderFile.write(pass.code) != pass.code.length()) +// { +// qWarning() << QStringLiteral("Could not write shader file data"); +// shaderFile.close(); +// return; +// } + +// shaderFile.close(); + +// //this is the common file +// if(pass.type == QStringLiteral("common")) +// continue; // wont have any inputs + +// const ShaderToyRenderOutput *channelOutput = nullptr; + +// for(const ShaderToyRenderOutput &output : std::as_const(pass.outputs)) +// { +// if(output.channel == 0) +// { +// channelOutput = &output; +// break; +// } +// } + +// QList channels(4); + +// QJsonObject *passObject = nullptr; + +// //this is the root shader +// if(pass.type == QStringLiteral("image")) +// { +// rootObject[QStringLiteral("source")] = QStringLiteral("./shaders/%1.frag.qsb").arg(pass.name); +// passObject = &rootObject; +// } +// else +// passObject = new QJsonObject; + +// for(const ShaderToyRenderInput &input : std::as_const(pass.inputs)) +// { +// /* +// * Only recursive buffers, images, videos and shader buffers are currently supported. +// * audio will default to audio capture +// */ + +// if(!m_supportedChannelTypes.contains(input.ctype)) +// { +// qWarning() << input.ctype << QStringLiteral(" is not a valid channel type"); +// continue; +// } + +// // recursive buffer reference +// if(channelOutput && input.id == channelOutput->id) +// { +// passObject->insert(QStringLiteral("frame_buffer_channel"), input.channel); +// continue; +// } + +// if(input.ctype == QStringLiteral("buffer")) +// { +// // get input reference by id +// const ShaderToyRenderPass *inputPass = nullptr; + +// for(const ShaderToyRenderPass &passSubScan : std::as_const(entry.renderPasses)) +// { +// for(const ShaderToyRenderOutput &output : std::as_const(passSubScan.outputs)) +// { +// if(output.id == input.id && output.channel == 0) +// { +// inputPass = &passSubScan; +// break; +// } + +// if(inputPass) +// break; +// } +// } + +// //whoopsie +// if(!inputPass) +// continue; + +// QString name = inputPass->name.toCaseFolded(); +// name.replace(name.length() - 1, 1, name.right(1).toUpper()); +// name.remove(QLatin1Char(' ')); +// name.replace(QStringLiteral("buf"), QStringLiteral("buffer")); + +// channels[input.channel][QStringLiteral("source")] = QStringLiteral("{%1}").arg(name); +// } + +// else if(input.ctype == QStringLiteral("audio")) +// channels[input.channel][QStringLiteral("type")] = 4; + +// else if(input.ctype == QStringLiteral("texture")) +// { +// QString filename = input.source; +// filename = filename.mid(filename.lastIndexOf(QLatin1Char('/')) + 1); + +// channels[input.channel][QStringLiteral("type")] = 0; +// channels[input.channel][QStringLiteral("source")] = QStringLiteral("./images/%1").arg(filename); + +// externalMedia.insert(imageDirectory.absoluteFilePath(filename), input.source); +// } + +// //select video file after compilation +// else if(input.ctype == QStringLiteral("video")) +// { +// //set the channel source to a uuid then add that uuid to the video +// // selection stringlist +// QString sourceName = QUuid::createUuidV7().toString(); +// channels[input.channel][QStringLiteral("type")] = 1; +// channels[input.channel][QStringLiteral("source")] = sourceName; + +// QStringList newSelections = m_videoSelections; +// newSelections += sourceName; + +// setVideoSelections(newSelections); +// } + +// channels[input.channel][QStringLiteral("filter")] = input.filter; +// channels[input.channel][QStringLiteral("wrap")] = input.wrap; +// channels[input.channel][QStringLiteral("invert")] = input.verticalFlip; +// channels[input.channel][QStringLiteral("srgb")] = input.srgb; +// channels[input.channel][QStringLiteral("internal")] = input.internal; +// } + +// for(int i = 0; i < 4; ++i) +// { +// if(channels[i].isEmpty()) +// continue; + +// passObject->insert(QStringLiteral("channel%1").arg(i), channels[i]); +// } + +// //this is a buffer +// if(pass.type == QStringLiteral("buffer")) +// { +// QString name = pass.name.toCaseFolded(); +// name.replace(name.length() - 1, 1, name.right(1).toUpper()); +// name.remove(QLatin1Char(' ')); +// name.replace(QStringLiteral("buf"), QStringLiteral("buffer")); +// passObject->insert(QStringLiteral("source"), QStringLiteral("./shaders/%1.frag.qsb").arg(passName)); + +// rootObject[name] = *passObject; +// } + +// if(*passObject != rootObject) +// delete passObject; +// } + +// QFile shaderPackFile(directory.absoluteFilePath(QStringLiteral("pack.json"))); + +// if(!shaderPackFile.open(QFile::WriteOnly)) +// { +// qWarning() << QStringLiteral("Could not open pack file"); +// return; +// } + +// QJsonDocument packDocument; +// packDocument.setObject(rootObject); + +// QByteArray jsonData = packDocument.toJson(QJsonDocument::Indented); + +// if(shaderPackFile.write(jsonData) != jsonData.length()) +// { +// qWarning() << QStringLiteral("Could not write pack data"); +// return; +// } + +// const QStringList keys = externalMedia.keys(); + +// // qWarning() << QStringLiteral("Downloading %1 Images").arg(externalMedia.count()); +// // setStatus(Compiling, QStringLiteral("Downloading images")); + +// setTotalDownloads(externalMedia.count()); + +// for(const QString &key : keys) +// downloadMedia(key, externalMedia[key]); +// } + +auto DownloadManager::install(const QUrl &uri) noexcept(false) -> QUrl +{ + if(!uri.isLocalFile()) + { + throw FileException(QStringLiteral("URI is not a local file")); + } + + QDir tempDirectory(uri.toLocalFile()); + + QString installLocation = QStringLiteral("%1/.local/share/komplex/packs/%2").arg + ( + QStandardPaths::writableLocation(QStandardPaths::HomeLocation), + tempDirectory.dirName() + ); + + QDir installDirectory(installLocation); + + if(installDirectory.exists()) + installDirectory.removeRecursively(); + + QProcess process; + + QObject::connect + ( + &process, + &QProcess::readyReadStandardOutput, + this, + [this, &process]() + { + QByteArray processData = process.readAllStandardOutput(); + + if(!processData.isValidUtf8()) + { + qWarning() << QStringLiteral("Process output not valid UTF8 data"); + return; + } + + setCompilerOutput(m_compilerOutput + processData); + } + ); + + QObject::connect + ( + &process, + &QProcess::readyReadStandardError, + this, + [this, &process]() + { + QByteArray processData = process.readAllStandardError(); + + if(!processData.isValidUtf8()) + { + qWarning() << QStringLiteral("Process output not valid UTF8 data"); + return; + } + + setCompilerOutput(m_compilerOutput + processData); + } + ); + + QStringList arguments = QStringList + { + QStringLiteral("-R"), + uri.toLocalFile(), + installLocation + }; + + process.start(QStringLiteral("cp"), arguments); + + if(!process.waitForStarted(3000)) + { + qWarning() << QStringLiteral("Could not start copy process: %1").arg(process.readAllStandardError()); + throw FileException(QStringLiteral("Could not start install process")); + } + + if(!process.waitForFinished()) + { + qWarning() << QStringLiteral("Copy process took longer than expected (>30s)"); + throw FileException(QStringLiteral("Install process took longer than expected (>30s)")); + } + + QUrl installUri(installLocation); + installUri.setScheme(QStringLiteral("file://")); + + return installUri; +} + +auto DownloadManager::download(const QNetworkRequest &request, const QString &id) noexcept(false) -> QUrl +{ + QEventLoop loop; + + QUrl downloadUri = QStringLiteral("%1/%2").arg + ( + QStandardPaths::writableLocation(QStandardPaths::TempLocation), + request.url().path().split('/', Qt::SkipEmptyParts).last() + ); + + QWeakPointer reference = CoreServices::networkAccessManager(); + QSharedPointer manager = reference.toStrongRef(); + + if(manager == nullptr) + { + throw NetworkException + ( + QStringLiteral("Network Manager reference has already been deleted") + ); + } + + QFile downloadFile(downloadUri.toLocalFile()); + + if(!downloadFile.open(QFile::ReadWrite)) + { + throw FileException(QStringLiteral("Could not open temp file location")); + } + + QNetworkReply *reply = manager->post(request, nullptr); + + QObject::connect + ( + reply, + &QNetworkReply::finished, + &loop, + &QEventLoop::quit + ); + + QObject::connect + ( + reply, + &QNetworkReply::errorOccurred, + &loop, + [&loop](QNetworkReply::NetworkError error) -> void + { + loop.quit(); + + throw NetworkException + ( + QStringLiteral("Network Error %1").arg + ( + QString::number(static_cast(error)) + ) + ); + } + ); + + QObject::connect + ( + reply, + &QNetworkReply::downloadProgress, + this, + [this](qint64 bytesDownloaded, qint64 bytesTotal) + { + setDownloadProgress(static_cast(bytesDownloaded) / bytesTotal); + } + ); + + QObject::connect + ( + reply, + &QNetworkReply::readyRead, + this, + [this, &downloadFile, &reply]() + { + if(!downloadFile.isOpen()) + { + return; + } + + quint64 bytes = reply->bytesAvailable(); + quint64 bytesWritten = downloadFile.write(reply->read(bytes)); + + if(bytesWritten != bytes) + { + throw FileException(QStringLiteral("Could not write temp file data")); + } + } + ); + + if(!reply->isFinished()) + { + loop.exec(); + } + + if(reply->bytesAvailable() > 0) + { + quint64 bytes = reply->bytesAvailable(); + quint64 bytesWritten = downloadFile.write(reply->read(bytes)); + + if(bytesWritten != bytes) + { + throw FileException(QStringLiteral("Could not write temp file data")); + } + } + + downloadFile.close(); + manager.clear(); + + return downloadUri; +} + +auto DownloadManager::readShaderToyEntry(const QUrl &uri) noexcept(false) -> ShaderToyEntry +{ + if(!uri.isLocalFile()) + { + throw FileException(QStringLiteral("URI is not a local file")); + } + + QDir packDir(uri.toLocalFile()); + + if(!packDir.exists(QStringLiteral("pack.json"))) + { + throw FileException(QStringLiteral("Pack file not found")); + } + + QFile packFile(packDir.absoluteFilePath(QStringLiteral("pack.json"))); + + if(!packFile.open(QFile::ReadOnly)) + { + throw FileException + ( + QStringLiteral("Could not open pack file: %1").arg + ( + packFile.errorString() + ) + ); + } + + QByteArray data = packFile.readAll(); + QJsonParseError jsonError; + + QJsonDocument document = QJsonDocument::fromJson(data, &jsonError); + + if(jsonError.error != QJsonParseError::NoError) + { + throw FileException + ( + QStringLiteral("Could parse pack file: %1").arg + ( + jsonError.errorString() + ) + ); + } + + QJsonObject documentObject = document.object(); + QJsonObject rootObject = documentObject[QStringLiteral("Shader")].toObject(); + QJsonObject infoObject = rootObject[QStringLiteral("info")].toObject(); + QJsonArray tagsArray = infoObject[QStringLiteral("tags")].toArray(); + + QStringList tags; + + for(const QJsonValue &tag : std::as_const(tagsArray)) + tags += tag.toString(); + + ShaderToyEntry entry; + + entry.metadata = ShaderToyMetadata + { + QDateTime::fromSecsSinceEpoch(infoObject[QStringLiteral("date")].toInt()), + infoObject[QStringLiteral("description")].toString(), + static_cast(infoObject[QStringLiteral("flags")].toInt()), + static_cast(infoObject[QStringLiteral("hasLiked")].toInt()), + infoObject[QStringLiteral("id")].toString(), + static_cast(infoObject[QStringLiteral("likes")].toInt()), + infoObject[QStringLiteral("name")].toString(), + static_cast(infoObject[QStringLiteral("published")].toInt()), + tags, + static_cast(infoObject[QStringLiteral("usePreview")].toInt()), + infoObject[QStringLiteral("username")].toString(), + rootObject[QStringLiteral("ver")].toString(), + static_cast(infoObject[QStringLiteral("views")].toInt()) + }; + + QJsonArray ShaderToyRenderPassArray = rootObject[QStringLiteral("renderpass")].toArray(); + + for(const QJsonValue &ShaderToyRenderPassValue : std::as_const(ShaderToyRenderPassArray)) + { + QJsonArray inputArray = ShaderToyRenderPassValue[QStringLiteral("inputs")].toArray(); + QJsonArray outputArray = ShaderToyRenderPassValue[QStringLiteral("outputs")].toArray(); + QList inputs; + QList outputs; + + for(const QJsonValue &inputValue : std::as_const(inputArray)) + { + QJsonObject samplerObject = inputValue[QStringLiteral("sampler")].toObject(); + inputs.append + ( + ShaderToyRenderInput + { + static_cast(inputValue[QStringLiteral("channel")].toInt()), + inputValue[QStringLiteral("ctype")].toString(), + samplerObject[QStringLiteral("filter")].toString(), + static_cast(inputValue[QStringLiteral("id")].toInt()), + samplerObject[QStringLiteral("internal")].toString(), + static_cast(inputValue[QStringLiteral("published")].toInt()), + inputValue[QStringLiteral("src")].toString(), + samplerObject[QStringLiteral("srgb")].toBool(), + samplerObject[QStringLiteral("verticalFlip")].toBool(), + samplerObject[QStringLiteral("wrap")].toString() + } + ); + } + + for(const QJsonValue &outputValue : std::as_const(outputArray)) + { + outputs.append + ( + ShaderToyRenderOutput + { + static_cast(outputValue[QStringLiteral("channel")].toInt()), + static_cast(outputValue[QStringLiteral("id")].toInt()) + } + ); + } + + entry.renderPasses.append + ( + ShaderToyRenderPass + { + ShaderToyRenderPassValue[QStringLiteral("code")].toVariant().toByteArray(), + ShaderToyRenderPassValue[QStringLiteral("description")].toString(), + inputs, + ShaderToyRenderPassValue[QStringLiteral("name")].toString(), + outputs, + ShaderToyRenderPassValue[QStringLiteral("type")].toString() + } + ); + } + + return std::move(entry); +} \ No newline at end of file diff --git a/KomplexHubPlugin/downloadmanager.h b/KomplexHubPlugin/downloadmanager.h new file mode 100644 index 0000000..cee0fdb --- /dev/null +++ b/KomplexHubPlugin/downloadmanager.h @@ -0,0 +1,104 @@ +#ifndef DOWNLOADMANAGER_H +#define DOWNLOADMANAGER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/komplex_global.h" +#include "common/shadertoymetadata.h" +#include "common/shaderpackmetadata.h" + +class KOMPLEX_EXPORT DownloadManager : public QObject +{ + Q_OBJECT + QML_ELEMENT +public: + + enum State + { + Idle, + Downloading, + Compiling, + Installing, + Complete, + Error + }; + Q_ENUM(State) + + explicit DownloadManager(QObject *parent = nullptr); + + /** + * @brief downloadImage + * Downloads the image to tmp from the given url + * @param url + */ + auto downloadImage(const QString &author, const QString &authorId, const QString &description, const QUrl &url) noexcept(false) -> void; + + /** + * @brief downloadPack + * Attempts to download, compile and install the wallpaper + * pack from the komplex endpoint. + * @param id + * @return File info of the installed pack + */ + auto downloadPack(const QString &id) noexcept(false) -> void; + + auto compilerOutput() const -> const QString & { return m_compilerOutput; } + auto errorTitle() const -> const QString & { return m_errorTitle; } + auto errorMessage() const -> const QString & { return m_errorMessage; } + auto state() const -> State { return m_state; } + auto reset() -> void; + auto downloadProgress() -> qreal { return m_downloadProgress; } + auto compileProgress() -> qreal { return m_compileProgress; } + +protected: + auto setError(const QString &title, const QString &message) -> void; + auto setCompilerOutput(const QString &compilerOutput) -> void; + auto setState(State state) -> void; + auto setDownloadProgress(qreal progress) -> void; + +signals: + auto compilerOutputChanged() -> void; + auto errorChanged() -> void; + auto stateChanged() -> void; + auto downloadProgressChanged() -> void; + auto compileProgressChanged() -> void; + auto downloadComplete(const QString &uri) -> void; + +private: + auto decompress(const QUrl &uri) noexcept(false) -> QUrl; + auto compile(const QUrl &uri) noexcept(false) -> QUrl; + auto install(const QUrl &uri) noexcept(false) -> QUrl; + auto download(const QNetworkRequest &request, const QString &id) noexcept(false) -> QUrl; + auto readShaderToyEntry(const QUrl &uri) noexcept(false) -> ShaderToyEntry; + + QString m_compilerOutput; + QString m_errorTitle; + QString m_errorMessage; + + qreal m_downloadProgress = 0; + qreal m_compileProgress = 0; + State m_state; + + QMutex m_downloadMutex; + + Q_PROPERTY(QString compilerOutput READ compilerOutput WRITE setCompilerOutput NOTIFY compilerOutputChanged FINAL) + Q_PROPERTY(QString errorTitle READ errorTitle NOTIFY errorChanged FINAL) + Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorChanged FINAL) + Q_PROPERTY(qreal downloadProgress READ downloadProgress NOTIFY downloadProgressChanged FINAL) + Q_PROPERTY(qreal compileProgress READ compileProgress NOTIFY compileProgressChanged FINAL) + Q_PROPERTY(State state READ state NOTIFY stateChanged FINAL) +}; +Q_DECLARE_METATYPE(DownloadManager) + +#endif // DOWNLOADMANAGER_H diff --git a/KomplexHubPlugin/featuredimagesmodel.cpp b/KomplexHubPlugin/featuredimagesmodel.cpp index f0a232f..fa38c0c 100644 --- a/KomplexHubPlugin/featuredimagesmodel.cpp +++ b/KomplexHubPlugin/featuredimagesmodel.cpp @@ -55,6 +55,14 @@ FeaturedImagesModel::FeaturedImagesModel(QObject *parent) : QAbstractListModel{p ); } +FeaturedImagesModel::~FeaturedImagesModel() +{ + if(m_paginator) + { + delete m_paginator; + } +} + auto FeaturedImagesModel::rowCount(const QModelIndex &) const -> int { if(m_paginator != nullptr) @@ -94,49 +102,36 @@ auto FeaturedImagesModel::data(const QModelIndex &index, int role) const -> QVar data = dataPoint.altText; break; case ThumbnailRole: - data = dataPoint.sources.value(QString::fromUtf8("tiny")); + data = dataPoint.sources.value(QString::fromUtf8("thumbnail")); + break; + case LargeThumbnailRole: + data = dataPoint.sources.value(QString::fromUtf8("largeThumbnail")); break; case PortraitUrlRole: data = dataPoint.sources.value(QString::fromUtf8("portrait")); break; - case LandscapeUrlRole: - data = dataPoint.sources.value(QString::fromUtf8("landscape")); + case PortraitSizeRole: + data = dataPoint.sourceSizes.value(QString::fromUtf8("portrait")); break; - case SmallUrlRole: - data = dataPoint.sources.value(QString::fromUtf8("small")); + case ScreenUrlRole: + data = dataPoint.sources.value(QString::fromUtf8("screen")); + break; + case ScreenSizeRole: + data = dataPoint.sourceSizes.value(QString::fromUtf8("screen")); break; case OriginalUrlRole: data = dataPoint.sources.value(QString::fromUtf8("original")); break; - case MediumUrlRole: - data = dataPoint.sources.value(QString::fromUtf8("medium")); - break; - case LargeUrlRole: - data = dataPoint.sources.value(QString::fromUtf8("large")); - break; - case ExtraLargeUrlRole: - data = dataPoint.sources.value(QString::fromUtf8("large2x")); - break; - case PortraitSizeRole: - data = dataPoint.sourceSizes.value(QString::fromUtf8("portrait")); + case LandscapeUrlRole: + data = dataPoint.sources.value(QString::fromUtf8("landscape")); 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")); + case BackgroundPortraitRole: + data = dataPoint.sources.value(QString::fromUtf8("backgroundPortrait")); + case BackgroundLandscapeRole: + data = dataPoint.sources.value(QString::fromUtf8("backgroundLandscape")); break; } @@ -250,13 +245,43 @@ auto FeaturedImagesModel::resultsPerPage() const -> qsizetype auto FeaturedImagesModel::setResultsPerPage(qsizetype resultsPerPage) -> void { + if(m_paginator != nullptr) { 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(); + } } ); } diff --git a/KomplexHubPlugin/featuredimagesmodel.h b/KomplexHubPlugin/featuredimagesmodel.h index 1fed04f..add3097 100644 --- a/KomplexHubPlugin/featuredimagesmodel.h +++ b/KomplexHubPlugin/featuredimagesmodel.h @@ -69,24 +69,21 @@ public: AuthorUrlRole, DescriptionRole, ThumbnailRole, + LargeThumbnailRole, + OriginalUrlRole, + ScreenUrlRole, PortraitUrlRole, LandscapeUrlRole, - SmallUrlRole, - OriginalUrlRole, - MediumUrlRole, - LargeUrlRole, - ExtraLargeUrlRole, + ScreenSizeRole, PortraitSizeRole, LandscapeSizeRole, - SmallSizeRole, - OriginalSizeRole, - MediumSizeRole, - LargeSizeRole, - ExtraLargeSizeRole + BackgroundPortraitRole, + BackgroundLandscapeRole }; Q_ENUM(DataRole) explicit FeaturedImagesModel(QObject *parent = nullptr); + ~FeaturedImagesModel(); auto rowCount(const QModelIndex &parent = QModelIndex()) const -> int override; @@ -295,6 +292,14 @@ private: static_cast(ThumbnailRole), QByteArray("thumbnail") }, + { + static_cast(LargeThumbnailRole), + QByteArray("largeThumbnail") + }, + { + static_cast(OriginalUrlRole), + QByteArray("original") + }, { static_cast(PortraitUrlRole), QByteArray("portrait") @@ -304,24 +309,20 @@ private: QByteArray("landscape") }, { - static_cast(SmallUrlRole), - QByteArray("small") + static_cast(BackgroundPortraitRole), + QByteArray("backgroundPortrait") }, { - static_cast(OriginalUrlRole), - QByteArray("original") + static_cast(BackgroundLandscapeRole), + QByteArray("backgroundLandscape") }, { - static_cast(MediumUrlRole), - QByteArray("medium") + static_cast(ScreenUrlRole), + QByteArray("fullScreen") }, { - static_cast(LargeUrlRole), - QByteArray("large") - }, - { - static_cast(ExtraLargeUrlRole), - QByteArray("extraLarge") + static_cast(ScreenSizeRole), + QByteArray("fullScreenSize") }, { static_cast(PortraitSizeRole), @@ -330,26 +331,6 @@ private: { 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; diff --git a/KomplexHubPlugin/featuredpacksmodel.cpp b/KomplexHubPlugin/featuredpacksmodel.cpp index 5e7507c..de2a415 100644 --- a/KomplexHubPlugin/featuredpacksmodel.cpp +++ b/KomplexHubPlugin/featuredpacksmodel.cpp @@ -56,6 +56,14 @@ FeaturedPacksModel::FeaturedPacksModel(QObject *parent) : QAbstractListModel{par ); } +FeaturedPacksModel::~FeaturedPacksModel() +{ + if(m_paginator) + { + delete m_paginator; + } +} + auto FeaturedPacksModel::rowCount(const QModelIndex &) const -> int { if(m_paginator != nullptr) @@ -181,6 +189,16 @@ auto FeaturedPacksModel::onPaginatorFetching() -> void setState(Loading); } +auto FeaturedPacksModel::boundaryCheck(qsizetype index) const -> bool +{ + if(index < 0 || m_paginator == nullptr || index >= m_paginator->count()) + { + return false; + } + + return true; +} + auto FeaturedPacksModel::roleNames() const -> QHash { return m_dataRoles; @@ -220,7 +238,36 @@ auto FeaturedPacksModel::setResultsPerPage(qsizetype resultsPerPage) -> void ( [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(); + } } ); } diff --git a/KomplexHubPlugin/featuredpacksmodel.h b/KomplexHubPlugin/featuredpacksmodel.h index 24dcab8..24ae662 100644 --- a/KomplexHubPlugin/featuredpacksmodel.h +++ b/KomplexHubPlugin/featuredpacksmodel.h @@ -78,6 +78,7 @@ public: Q_ENUM(DataRole) explicit FeaturedPacksModel(QObject *parent = nullptr); + ~FeaturedPacksModel(); auto rowCount(const QModelIndex &parent = QModelIndex()) const -> int override; @@ -239,6 +240,14 @@ 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 diff --git a/KomplexHubPlugin/featuredvideosmodel.cpp b/KomplexHubPlugin/featuredvideosmodel.cpp index 77ffd6f..2344376 100644 --- a/KomplexHubPlugin/featuredvideosmodel.cpp +++ b/KomplexHubPlugin/featuredvideosmodel.cpp @@ -3,6 +3,8 @@ FeaturedVideosModel::FeaturedVideosModel(QObject *parent) : QAbstractListModel{parent} { + m_itemModel = new VideoItemModel(this); + m_paginator = new FeaturedVideosPaginator(this); PaginationNotifier *notifier = static_cast(m_paginator); @@ -56,6 +58,19 @@ FeaturedVideosModel::FeaturedVideosModel(QObject *parent) : QAbstractListModel{p ); } +FeaturedVideosModel::~FeaturedVideosModel() +{ + if(m_paginator) + { + delete m_paginator; + } + + if(m_itemModel) + { + delete m_itemModel; + } +} + auto FeaturedVideosModel::rowCount(const QModelIndex &) const -> int { if(m_paginator != nullptr) @@ -68,7 +83,7 @@ auto FeaturedVideosModel::rowCount(const QModelIndex &) const -> int auto FeaturedVideosModel::data(const QModelIndex &index, int role) const -> QVariant { - if(index.row() < 0 || m_paginator == nullptr || index.row() >= m_paginator->count()) + if(!boundaryCheck(index.row())) { return {}; } @@ -115,12 +130,7 @@ auto FeaturedVideosModel::index(int row, int column, const QModelIndex &parent) { Q_UNUSED(parent) - if(m_paginator == nullptr) - { - return {}; - } - - if(row < 0 || row >= m_paginator->count()) + if(!boundaryCheck(row)) { return {}; } @@ -180,6 +190,21 @@ auto FeaturedVideosModel::onPaginatorFetching() -> void setState(Loading); } +auto FeaturedVideosModel::boundaryCheck(qsizetype index) const -> bool +{ + if(index < 0 || m_paginator == nullptr || index >= m_paginator->count()) + { + return false; + } + + return true; +} + +auto FeaturedVideosModel::itemModel() const -> VideoItemModel* +{ + return m_itemModel; +} + auto FeaturedVideosModel::roleNames() const -> QHash { return m_dataRoles; @@ -219,7 +244,36 @@ auto FeaturedVideosModel::setResultsPerPage(qsizetype resultsPerPage) -> void ( [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(); + } } ); } @@ -263,6 +317,17 @@ auto FeaturedVideosModel::previousPage() -> void } } +auto FeaturedVideosModel::setItem(qint64 index) -> bool +{ + if(!boundaryCheck(index)) + { + return false; + } + + m_itemModel->setDataEntry(m_paginator->at(index)); + return true; +} + qsizetype FeaturedVideosModel::page() const { if(m_paginator != nullptr) diff --git a/KomplexHubPlugin/featuredvideosmodel.h b/KomplexHubPlugin/featuredvideosmodel.h index 9b0a734..a818680 100644 --- a/KomplexHubPlugin/featuredvideosmodel.h +++ b/KomplexHubPlugin/featuredvideosmodel.h @@ -38,6 +38,7 @@ #include #include "paginators/featuredvideospaginator.h" +#include "videoitemmodel.h" /** * @brief The FeaturedVideosModel class @@ -76,6 +77,7 @@ public: Q_ENUM(DataRole) explicit FeaturedVideosModel(QObject *parent = nullptr); + ~FeaturedVideosModel(); auto rowCount(const QModelIndex &parent = QModelIndex()) const -> int override; @@ -205,6 +207,16 @@ public: */ Q_INVOKABLE auto previousPage() -> void; + /** + * @brief setItem + * Sets the current itemModel to the item at the specified index + * @param index + * @return + */ + Q_INVOKABLE auto setItem(qint64 index) -> bool; + + VideoItemModel *itemModel() const; + protected: /** * @brief setErrorString @@ -237,6 +249,14 @@ 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 @@ -285,6 +305,7 @@ private: QString m_errorString = QString(); FeaturedVideosPaginator *m_paginator = nullptr; + VideoItemModel *m_itemModel = 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) @@ -292,6 +313,7 @@ private: 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(VideoItemModel *itemModel READ itemModel CONSTANT FINAL) }; Q_DECLARE_METATYPE(FeaturedVideosModel) diff --git a/KomplexHubPlugin/imagesearchmodel.cpp b/KomplexHubPlugin/imagesearchmodel.cpp index 5e8b7b4..794ea87 100644 --- a/KomplexHubPlugin/imagesearchmodel.cpp +++ b/KomplexHubPlugin/imagesearchmodel.cpp @@ -109,49 +109,36 @@ auto ImageSearchModel::data(const QModelIndex &index, int role) const -> QVarian data = dataPoint.altText; break; case ThumbnailRole: - data = dataPoint.sources.value(QString::fromUtf8("tiny")); + data = dataPoint.sources.value(QString::fromUtf8("thumbnail")); + break; + case LargeThumbnailRole: + data = dataPoint.sources.value(QString::fromUtf8("largeThumbnail")); break; case PortraitUrlRole: data = dataPoint.sources.value(QString::fromUtf8("portrait")); break; - case LandscapeUrlRole: - data = dataPoint.sources.value(QString::fromUtf8("landscape")); + case PortraitSizeRole: + data = dataPoint.sourceSizes.value(QString::fromUtf8("portrait")); break; - case SmallUrlRole: - data = dataPoint.sources.value(QString::fromUtf8("small")); + case ScreenUrlRole: + data = dataPoint.sources.value(QString::fromUtf8("screen")); + break; + case ScreenSizeRole: + data = dataPoint.sourceSizes.value(QString::fromUtf8("screen")); break; case OriginalUrlRole: data = dataPoint.sources.value(QString::fromUtf8("original")); break; - case MediumUrlRole: - data = dataPoint.sources.value(QString::fromUtf8("medium")); - break; - case LargeUrlRole: - data = dataPoint.sources.value(QString::fromUtf8("large")); - break; - case ExtraLargeUrlRole: - data = dataPoint.sources.value(QString::fromUtf8("large2x")); - break; - case PortraitSizeRole: - data = dataPoint.sourceSizes.value(QString::fromUtf8("portrait")); + case LandscapeUrlRole: + data = dataPoint.sources.value(QString::fromUtf8("landscape")); 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")); + case BackgroundPortraitRole: + data = dataPoint.sources.value(QString::fromUtf8("backgroundPortrait")); + case BackgroundLandscapeRole: + data = dataPoint.sources.value(QString::fromUtf8("backgroundLandscape")); break; } @@ -222,6 +209,16 @@ auto ImageSearchModel::onPaginatorFetching() -> void setState(Loading); } +auto ImageSearchModel::boundaryCheck(qsizetype index) const -> bool +{ + if(index < 0 || m_paginator == nullptr || index >= m_paginator->count()) + { + return false; + } + + return true; +} + auto ImageSearchModel::hasNextPage() const -> bool { return m_paginator->page() < m_paginator->totalPages(); diff --git a/KomplexHubPlugin/imagesearchmodel.h b/KomplexHubPlugin/imagesearchmodel.h index 250a4c0..4a26e03 100644 --- a/KomplexHubPlugin/imagesearchmodel.h +++ b/KomplexHubPlugin/imagesearchmodel.h @@ -69,20 +69,16 @@ public: AuthorUrlRole, DescriptionRole, ThumbnailRole, + LargeThumbnailRole, + OriginalUrlRole, + ScreenUrlRole, PortraitUrlRole, LandscapeUrlRole, - SmallUrlRole, - OriginalUrlRole, - MediumUrlRole, - LargeUrlRole, - ExtraLargeUrlRole, + ScreenSizeRole, PortraitSizeRole, LandscapeSizeRole, - SmallSizeRole, - OriginalSizeRole, - MediumSizeRole, - LargeSizeRole, - ExtraLargeSizeRole + BackgroundPortraitRole, + BackgroundLandscapeRole }; Q_ENUM(DataRole) @@ -322,93 +318,85 @@ signals: auto queryChanged() -> 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 = - { { - static_cast(UuidRole), - QByteArray("uuid") - }, - { - static_cast(AuthorRole), - QByteArray("author") - }, - { - static_cast(AuthorIdRole), - QByteArray("authorId") - }, - { - static_cast(AuthorUrlRole), - QByteArray("authorUrl") - }, - { - static_cast(DescriptionRole), - QByteArray("description") - }, - { - static_cast(ThumbnailRole), - QByteArray("thumbnail") - }, - { - static_cast(PortraitUrlRole), - QByteArray("portrait") - }, - { - static_cast(LandscapeUrlRole), - QByteArray("landscape") - }, - { - static_cast(SmallUrlRole), - QByteArray("small") - }, - { - static_cast(OriginalUrlRole), - QByteArray("original") - }, - { - static_cast(MediumUrlRole), - QByteArray("medium") - }, - { - static_cast(LargeUrlRole), - QByteArray("large") - }, - { - static_cast(ExtraLargeUrlRole), - QByteArray("extraLarge") - }, - { - 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") - } - }; + { + static_cast(UuidRole), + QByteArray("uuid") + }, + { + static_cast(AuthorRole), + QByteArray("author") + }, + { + static_cast(AuthorIdRole), + QByteArray("authorId") + }, + { + static_cast(AuthorUrlRole), + QByteArray("authorUrl") + }, + { + static_cast(DescriptionRole), + QByteArray("description") + }, + { + static_cast(ThumbnailRole), + QByteArray("thumbnail") + }, + { + static_cast(LargeThumbnailRole), + QByteArray("largeThumbnail") + }, + { + static_cast(OriginalUrlRole), + QByteArray("original") + }, + { + static_cast(PortraitUrlRole), + QByteArray("portrait") + }, + { + static_cast(LandscapeUrlRole), + QByteArray("landscape") + }, + { + static_cast(BackgroundPortraitRole), + QByteArray("backgroundPortrait") + }, + { + static_cast(BackgroundLandscapeRole), + QByteArray("backgroundLandscape") + }, + { + static_cast(ScreenUrlRole), + QByteArray("fullScreen") + }, + { + static_cast(ScreenSizeRole), + QByteArray("fullScreenSize") + }, + { + static_cast(PortraitSizeRole), + QByteArray("portraitSize") + }, + { + static_cast(LandscapeSizeRole), + QByteArray("landscapeSize") + } + }; State m_state = Idle; QString m_errorString = QString(); diff --git a/KomplexHubPlugin/newestpacksmodel.cpp b/KomplexHubPlugin/newestpacksmodel.cpp index 75e24b9..058d7d6 100644 --- a/KomplexHubPlugin/newestpacksmodel.cpp +++ b/KomplexHubPlugin/newestpacksmodel.cpp @@ -58,6 +58,14 @@ NewestPacksModel::NewestPacksModel(QObject *parent) : QAbstractListModel{parent} ); } +NewestPacksModel::~NewestPacksModel() +{ + if(m_paginator) + { + delete m_paginator; + } +} + auto NewestPacksModel::rowCount(const QModelIndex &) const -> int { if(m_paginator != nullptr) @@ -216,7 +224,42 @@ auto NewestPacksModel::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(); + } + } + ); } } diff --git a/KomplexHubPlugin/newestpacksmodel.h b/KomplexHubPlugin/newestpacksmodel.h index f87d796..70ecd71 100644 --- a/KomplexHubPlugin/newestpacksmodel.h +++ b/KomplexHubPlugin/newestpacksmodel.h @@ -78,6 +78,7 @@ public: Q_ENUM(DataRole) explicit NewestPacksModel(QObject *parent = nullptr); + ~NewestPacksModel(); /** * @brief rowCount @@ -249,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 = { { diff --git a/KomplexHubPlugin/packsearchmodel.cpp b/KomplexHubPlugin/packsearchmodel.cpp index 83aa256..7a8e40a 100644 --- a/KomplexHubPlugin/packsearchmodel.cpp +++ b/KomplexHubPlugin/packsearchmodel.cpp @@ -62,6 +62,14 @@ PackSearchModel::PackSearchModel(QObject *parent) : QAbstractListModel{parent} ); } +PackSearchModel::~PackSearchModel() +{ + if(m_paginator) + { + delete m_paginator; + } +} + auto PackSearchModel::rowCount(const QModelIndex &) const -> int { if(m_paginator != nullptr) @@ -185,6 +193,16 @@ auto PackSearchModel::resetDataModel() -> void endInsertRows(); } +auto PackSearchModel::boundaryCheck(qsizetype index) const -> bool +{ + if(index < 0 || m_paginator == nullptr || index >= m_paginator->count()) + { + return false; + } + + return true; +} + auto PackSearchModel::hasNextPage() const -> bool { return m_paginator->page() < m_paginator->totalPages(); @@ -239,7 +257,36 @@ auto PackSearchModel::setResultsPerPage(qsizetype resultsPerPage) -> void ( [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(); + } } ); } diff --git a/KomplexHubPlugin/packsearchmodel.h b/KomplexHubPlugin/packsearchmodel.h index f957ee5..107808c 100644 --- a/KomplexHubPlugin/packsearchmodel.h +++ b/KomplexHubPlugin/packsearchmodel.h @@ -78,6 +78,7 @@ public: Q_ENUM(DataRole) explicit PackSearchModel(QObject *parent = nullptr); + ~PackSearchModel(); auto rowCount(const QModelIndex &parent = QModelIndex()) const -> int override; @@ -311,6 +312,14 @@ signals: auto queryChanged() -> void; private: + /** + * @brief boundaryCheck + * Helper function to check the requested index boundary + * @param index + * @return + */ + auto boundaryCheck(qsizetype index) const -> bool; + /** * Data role map that connects PackSearchModel::DataRole to it's QML accessor name */ diff --git a/KomplexHubPlugin/paginators/featuredpackspaginator.h b/KomplexHubPlugin/paginators/featuredpackspaginator.h index 00a1741..d767036 100644 --- a/KomplexHubPlugin/paginators/featuredpackspaginator.h +++ b/KomplexHubPlugin/paginators/featuredpackspaginator.h @@ -40,7 +40,7 @@ public: QString::fromUtf8(KOMPLEX_ENDPOINT_PACKS_FEATURED) ); - controller()->setWindowSize(20); + controller()->setWindowSize(32); } }; diff --git a/KomplexHubPlugin/paginators/imagepaginator.h b/KomplexHubPlugin/paginators/imagepaginator.h index 50382da..1393bfe 100644 --- a/KomplexHubPlugin/paginators/imagepaginator.h +++ b/KomplexHubPlugin/paginators/imagepaginator.h @@ -152,30 +152,121 @@ protected: const QVariantMap sources = resultObject.value(QString::fromUtf8("src")).toObject().toVariantMap(); QMap sourceMap; - QMap sizeMap; QMapIterator sourceIterator(sources); + QString original; while(sourceIterator.hasNext()) { sourceIterator.next(); - QString url = sourceIterator.value().toString(); - sourceMap.insert(sourceIterator.key(), url); - - auto matches = s_sizeExpression.match(url); - - if(matches.hasMatch()) + if(sourceIterator.key().toLower() == QString::fromUtf8("original")) { - 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)); + original = sourceIterator.value().toString(); + break; } } + QSize screenSize = CoreServices::screenSize(); + QSize portraitSize(screenSize.height(),screenSize.width()); + QSize landscapeSize(screenSize.width(),screenSize.height()); + + if(screenSize.height() > screenSize.width()) + { + portraitSize = screenSize; + landscapeSize = QSize(screenSize.height(),screenSize.width()); + } + + sourceMap.clear(); + sourceMap = QMap + { + { + QString::fromUtf8("original"), + original + }, + { + QString::fromUtf8("screen"), + QString::fromUtf8("%1?auto=compress&cs=tinysrgb&fit=crop&h=%2&w=%3").arg( + original, + QString::number(screenSize.height()), + QString::number(screenSize.width()) + ) + }, + { + QString::fromUtf8("portrait"), + QString::fromUtf8("%1?auto=compress&cs=tinysrgb&fit=crop&h=%2&w=%3").arg( + original, + QString::number(portraitSize.height()), + QString::number(portraitSize.width()) + ) + }, + { + QString::fromUtf8("landscape"), + QString::fromUtf8("%1?auto=compress&cs=tinysrgb&fit=crop&h=%2&w=%3").arg( + original, + QString::number(landscapeSize.height()), + QString::number(landscapeSize.width()) + ) + }, + { + QString::fromUtf8("backgroundPortrait"), + QString::fromUtf8("%1?auto=compress&cs=tinysrgb&fit=crop&h=%2&w=%3").arg( + original, + QString::number(portraitSize.height() / 2), + QString::number(portraitSize.width() / 2) + ) + }, + { + QString::fromUtf8("backgroundLandscape"), + QString::fromUtf8("%1?auto=compress&cs=tinysrgb&fit=crop&h=%2&w=%3").arg( + original, + QString::number(landscapeSize.height() / 2), + QString::number(landscapeSize.width() / 2) + ) + }, + { + QString::fromUtf8("thumbnail"), + QString::fromUtf8("%1?auto=compress&cs=tinysrgb&fit=crop&h=144&w=256%3").arg( + original + ) + }, + { + QString::fromUtf8("largeThumbnail"), + QString::fromUtf8("%1?auto=compress&cs=tinysrgb&fit=crop&h=450&w=800%3").arg( + original + ) + } + }; + + QMap sizeMap = + { + { + QString::fromUtf8("original"), + QString() + }, + { + QString::fromUtf8("screen"), + QString::fromUtf8("%1x%2").arg( + QString::number(screenSize.width()), + QString::number(screenSize.height()) + ) + }, + { + QString::fromUtf8("portrait"), + QString::fromUtf8("%1x%2").arg( + QString::number(portraitSize.width()), + QString::number(portraitSize.height()) + ) + }, + { + QString::fromUtf8("landscape"), + QString::fromUtf8("%1x%2").arg( + QString::number(landscapeSize.width()), + QString::number(landscapeSize.height()) + ) + } + }; + fetchedResults.append( { resultObject.value(QString::fromUtf8("alt")).toString(), @@ -214,8 +305,8 @@ protected: auto getNetworkReply(const QNetworkRequest &request) const -> QNetworkReply* { QEventLoop loop; - QWeakPointer managerReference = CoreServices::networkAccessManager(); + QWeakPointer managerReference = CoreServices::networkAccessManager(); QSharedPointer manager = managerReference.toStrongRef(); LOG_ERROR_X(!manager, @@ -238,6 +329,8 @@ protected: loop.exec(); } + manager.clear(); + LOG_ERROR_X(reply->error() != QNetworkReply::NoError, "NewestPacksPaginator::getNetworkReply", reply->errorString().toStdString().c_str(), @@ -247,28 +340,6 @@ protected: return reply; } - /** - * @brief getDocument - * Extracts the JSON document from the server reply - * @param reply - * @return - */ - [[nodiscard]] - auto getDocument(QNetworkReply *reply) const -> QJsonDocument - { - QByteArray data = reply->readAll(); - QJsonParseError documentError; - QJsonDocument document = QJsonDocument::fromJson(data, &documentError); - - LOG_ERROR_X(documentError.error != QJsonParseError::NoError, - "NewestPacksPaginator::getDocument", - documentError.errorString().toStdString().c_str(), - {} - ); - - return document; - } - /** * @brief setUri * Sets the API Endpoint URI to fetch from diff --git a/KomplexHubPlugin/paginators/videopaginator.h b/KomplexHubPlugin/paginators/videopaginator.h index 566fa35..dfe39c8 100644 --- a/KomplexHubPlugin/paginators/videopaginator.h +++ b/KomplexHubPlugin/paginators/videopaginator.h @@ -254,28 +254,6 @@ protected: return reply; } - /** - * @brief getDocument - * Extracts the JSON document from the server reply - * @param reply - * @return - */ - [[nodiscard]] - auto getDocument(QNetworkReply *reply) const -> QJsonDocument - { - QByteArray data = reply->readAll(); - QJsonParseError documentError; - QJsonDocument document = QJsonDocument::fromJson(data, &documentError); - - LOG_ERROR_X(documentError.error != QJsonParseError::NoError, - "NewestPacksPaginator::getDocument", - documentError.errorString().toStdString().c_str(), - {} - ); - - return document; - } - /** * @brief setUri * Sets the API Endpoint URI to fetch from diff --git a/KomplexHubPlugin/paginators/wallpaperpaginator.h b/KomplexHubPlugin/paginators/wallpaperpaginator.h index e9cbe1e..4d6a166 100644 --- a/KomplexHubPlugin/paginators/wallpaperpaginator.h +++ b/KomplexHubPlugin/paginators/wallpaperpaginator.h @@ -62,35 +62,35 @@ 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 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; - } + // /** + // * @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(); + // m_query = query; + // Q_EMIT queryChanged(); - reset(); - setOffset(0); - } + // reset(); + // setOffset(0); + // } signals: /** @@ -120,7 +120,7 @@ protected: {} ); - if(queryable() && query().isEmpty()) + if(queryable() && query().isEmpty()) { return {}; } @@ -139,7 +139,7 @@ protected: { { QByteArray("query"), - m_query.toUtf8() + query().toUtf8() }, { QByteArray("offset"), @@ -303,11 +303,11 @@ private: */ QString m_uri; - /** - * @brief m_query - * The query string used in fetch operations - */ - QString m_query; + // /** + // * @brief m_query + // * The query string used in fetch operations + // */ + // QString m_query; }; #endif // WALLPAPERPAGINATOR_H diff --git a/KomplexHubPlugin/videoitemmodel.cpp b/KomplexHubPlugin/videoitemmodel.cpp new file mode 100644 index 0000000..8dab67b --- /dev/null +++ b/KomplexHubPlugin/videoitemmodel.cpp @@ -0,0 +1,203 @@ +#include "videoitemmodel.h" + +VideoItemModel::VideoItemModel(QObject *parent) : QAbstractListModel{parent} +{ + QObject::connect( + this, + &VideoItemModel::dataEntryChanged, + this, + &VideoItemModel::resetDataModel + ); +} + +auto VideoItemModel::rowCount(const QModelIndex &) const -> int +{ + return m_dataEntry.files.count(); +} + +auto VideoItemModel::data(const QModelIndex &index, int role) const -> QVariant +{ + if(!boundaryCheck(index.row())) + { + return {}; + } + + VideoEntry dataPoint = m_dataEntry.files.at(index.row()); + + QVariant data; + + switch(static_cast(role)) + { + case UuidRole: + data = dataPoint.id; + break; + case UrlRole: + data = dataPoint.url; + break; + case HeightRole: + data = dataPoint.height; + break; + case WidthRole: + data = dataPoint.width; + break; + case FileTypeRole: + data = dataPoint.type; + break; + case FpsRole: + data = dataPoint.fps; + break; + case SizeRole: + data = dataPoint.size; + break; + case QualityRole: + data = dataPoint.quality; + break; + case TextRole: + data = friendlyText(dataPoint); + break; + } + + return data; +} + +auto VideoItemModel::index(int row, int column, const QModelIndex &parent) const -> QModelIndex +{ + Q_UNUSED(parent) + + if(!boundaryCheck(row)) + { + return {}; + } + + return createIndex(row, column, &m_dataEntry.files[row]); +} + +auto VideoItemModel::columnCount(const QModelIndex &) const -> int +{ + return 0; +} + +auto VideoItemModel::parent(const QModelIndex &) const -> QModelIndex +{ + return {}; +} + +auto VideoItemModel::setData(const QModelIndex &, const QVariant &, int) -> bool +{ + return false; +} + +auto VideoItemModel::state() const -> VideoItemModel::State +{ + return m_state; +} + +auto VideoItemModel::setState(State state) -> void +{ + if (m_state == state) + { + return; + } + + m_state = state; + emit stateChanged(); +} + +auto VideoItemModel::resetState() -> void +{ + setState(Idle); +} + +auto VideoItemModel::resetDataModel() -> void +{ + if(m_lastCount > 0) + { + //invalidate previous model data + beginRemoveRows(QModelIndex(), 0, m_lastCount - 1); + endRemoveRows(); + } + + m_lastCount = m_dataEntry.files.count(); + + //signal new data + beginInsertRows(QModelIndex(), 0, m_dataEntry.files.count() - 1); + endInsertRows(); +} + +auto VideoItemModel::boundaryCheck(qsizetype index) const -> bool +{ + if(index < 0 || index >= m_dataEntry.files.count()) + { + return false; + } + + return true; +} + +auto VideoItemModel::setDataEntry(const VideoCache &entry) -> void +{ + if(entry == m_dataEntry) + { + return; + } + + m_dataEntry = entry; + Q_EMIT dataEntryChanged(); +} + +auto VideoItemModel::friendlyText(const VideoEntry &video) const -> QString +{ + qint64 size = video.size; + qint64 sizeIndex = 0; + + while(size >= 1000) + { + size = static_cast(std::ceil(static_cast(size) / 1000)); + ++sizeIndex; + } + + if(!video.quality.isEmpty()) + { + return QString("%1/%2 [%3x%4 %5FPS] (%6%7)").arg + ( + video.quality.toUpper(), + video.type.toUpper(), + QString::number(video.width), + QString::number(video.height), + QString::number(video.fps), + QString::number(size), + m_sizeTexts[sizeIndex] + ); + } + + return QString("%1 [%2x%3 %4FPS] (%5%6)").arg + ( + video.type.toUpper(), + QString::number(video.width), + QString::number(video.height), + QString::number(video.fps), + QString::number(size), + m_sizeTexts[sizeIndex] + ); +} + +auto VideoItemModel::roleNames() const -> QHash +{ + return m_dataRoles; +} + +auto VideoItemModel::errorString() const -> QString +{ + return m_errorString; +} + +auto VideoItemModel::setErrorString(const QString &errorString) -> void +{ + if (m_errorString == errorString) + { + return; + } + + m_errorString = errorString; + emit errorStringChanged(); +} \ No newline at end of file diff --git a/KomplexHubPlugin/videoitemmodel.h b/KomplexHubPlugin/videoitemmodel.h new file mode 100644 index 0000000..a93e8a6 --- /dev/null +++ b/KomplexHubPlugin/videoitemmodel.h @@ -0,0 +1,294 @@ +/* + * 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 VideoItemModel_H +#define VideoItemModel_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/komplex_global.h" +#include "common/videocache.h" + +/** + * @brief The VideoItemModel class + */ +class KOMPLEX_EXPORT VideoItemModel : 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, + FileTypeRole, + FpsRole, + HeightRole, + WidthRole, + SizeRole, + UrlRole, + QualityRole, + TextRole + }; + Q_ENUM(DataRole) + + explicit VideoItemModel(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 setDataEntry + * Set the dataEntry from the Search or List model + * @param entry + */ + auto setDataEntry(const VideoCache &entry) -> void; + + /** + * @brief friendlyText + * Generates the friendly text for comboboxes and the like + * @param video + * @return + */ + auto friendlyText(const VideoEntry &video) const -> QString; + +protected: + + /** + * @brief setErrorString + * @param errorString + */ + auto setErrorString(const QString &errorString) -> void; + + /** + * @brief setState + * @param state + */ + auto setState(State state) -> void; + + /** + * @brief resetState + */ + auto resetState() -> void; + + /** + * @brief resetDataModel + * Clears the data model and creates a new one if data exists + */ + auto resetDataModel() -> void; + +signals: + auto dataEntryChanged() -> void; + + /** + * @brief stateChanged + * Signaled when reported state changes + */ + auto stateChanged() -> void; + + /** + * @brief errorStringChanged + * Signaled when reported error message changes + */ + auto errorStringChanged() -> void; + +private: + /** + * @brief boundaryCheck + * Helper function to check the requested index boundary + * @param index + * @return + */ + auto boundaryCheck(qsizetype index) const -> bool; + + /** + * Size texts used in friendlyText() + */ + QStringList m_sizeTexts + { + QString::fromUtf8("B"), + QString::fromUtf8("KB"), + QString::fromUtf8("MB"), + QString::fromUtf8("GB") + }; + + /** + * Data role map that connects VideoItemModel::DataRole to it's QML accessor name + */ + static inline const QHash m_dataRoles = + { + { + static_cast(UuidRole), + QByteArray("uuid") + }, + { + static_cast(FileTypeRole), + QByteArray("fileType") + }, + { + static_cast(FpsRole), + QByteArray("fps") + }, + { + static_cast(HeightRole), + QByteArray("height") + }, + { + static_cast(WidthRole), + QByteArray("width") + }, + { + static_cast(SizeRole), + QByteArray("size") + }, + { + static_cast(UrlRole), + QByteArray("url") + }, + { + static_cast(QualityRole), + QByteArray("quality") + }, + { + static_cast(TextRole), + QByteArray("text") + } + }; + + State m_state = Idle; + QString m_errorString = QString(); + + VideoCache m_dataEntry; + qint64 m_lastCount = 0; + + 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_DECLARE_METATYPE(VideoItemModel) + +#endif // VideoItemModel_H diff --git a/KomplexHubPlugin/videosearchmodel.cpp b/KomplexHubPlugin/videosearchmodel.cpp index 737847e..f9c1960 100644 --- a/KomplexHubPlugin/videosearchmodel.cpp +++ b/KomplexHubPlugin/videosearchmodel.cpp @@ -2,6 +2,7 @@ VideoSearchModel::VideoSearchModel(QObject *parent) : QAbstractListModel{parent} { + m_itemModel = new VideoItemModel(this); m_paginator = new VideoSearchPaginator(this); PaginationNotifier *notifier = static_cast(m_paginator); @@ -62,6 +63,19 @@ VideoSearchModel::VideoSearchModel(QObject *parent) : QAbstractListModel{parent} ); } +VideoSearchModel::~VideoSearchModel() +{ + if(m_paginator) + { + delete m_paginator; + } + + if(m_itemModel) + { + delete m_itemModel; + } +} + auto VideoSearchModel::rowCount(const QModelIndex &) const -> int { if(m_paginator != nullptr) @@ -74,18 +88,12 @@ auto VideoSearchModel::rowCount(const QModelIndex &) const -> int auto VideoSearchModel::data(const QModelIndex &index, int role) const -> QVariant { - if(m_paginator == nullptr) - { - return {}; - } - - if(index.row() < 0 || index.row() >= m_paginator->count() ) + if(!boundaryCheck(index.row())) { return {}; } VideoCache dataPoint = m_paginator->at(index.row()); - QVariant data; switch(static_cast(role)) @@ -126,8 +134,10 @@ auto VideoSearchModel::index(int row, int column, const QModelIndex &parent) con { Q_UNUSED(parent) - if(row < 0 || row >= m_paginator->count()) + if(!boundaryCheck(row)) + { return {}; + } return createIndex(row, column, &m_paginator[row]); } @@ -171,14 +181,45 @@ auto VideoSearchModel::resetState() -> void auto VideoSearchModel::resetDataModel() -> void { //invalidate previous model data - beginRemoveRows(QModelIndex(), 0, m_paginator->count()); - endRemoveRows(); + if(m_lastCount > 0) + { + beginRemoveRows(QModelIndex(), 0, m_lastCount - 1); + endRemoveRows(); + } + + m_lastCount = m_paginator->count(); //signal new data beginInsertRows(QModelIndex(), 0, m_paginator->count() - 1); endInsertRows(); } +auto VideoSearchModel::setItem(qint64 index) -> bool +{ + if(!boundaryCheck(index)) + { + return false; + } + + m_itemModel->setDataEntry(m_paginator->at(index)); + return true; +} + +auto VideoSearchModel::boundaryCheck(qsizetype index) const -> bool +{ + if(index < 0 || m_paginator == nullptr || index >= m_paginator->count()) + { + return false; + } + + return true; +} + +auto VideoSearchModel::itemModel() const -> VideoItemModel* +{ + return m_itemModel; +} + auto VideoSearchModel::hasNextPage() const -> bool { return m_paginator->page() < m_paginator->totalPages(); @@ -233,7 +274,37 @@ auto VideoSearchModel::setResultsPerPage(qsizetype resultsPerPage) -> void ( [this, resultsPerPage] { + qsizetype difference = resultsPerPage - m_paginator->resultsPerPage(); m_paginator->setResultsPerPage(resultsPerPage); + m_lastCount = m_paginator->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(); + } } ); } diff --git a/KomplexHubPlugin/videosearchmodel.h b/KomplexHubPlugin/videosearchmodel.h index a756b73..1d1ca0f 100644 --- a/KomplexHubPlugin/videosearchmodel.h +++ b/KomplexHubPlugin/videosearchmodel.h @@ -38,6 +38,7 @@ #include #include "paginators/videosearchpaginator.h" +#include "videoitemmodel.h" /** * @brief The VideoSearchModel class @@ -76,6 +77,7 @@ public: Q_ENUM(DataRole) explicit VideoSearchModel(QObject *parent = nullptr); + ~VideoSearchModel(); auto rowCount(const QModelIndex &parent = QModelIndex()) const -> int override; @@ -233,6 +235,16 @@ public: */ auto hasPreviousPage() const -> bool; + /** + * @brief setItem + * Sets the current itemModel to the item at the specified index + * @param index + * @return + */ + Q_INVOKABLE auto setItem(qint64 index) -> bool; + + VideoItemModel *itemModel() const; + protected: /** @@ -309,6 +321,14 @@ signals: auto queryChanged() -> void; private: + /** + * @brief boundaryCheck + * Helper function to check the requested index boundary + * @param index + * @return + */ + auto boundaryCheck(qsizetype index) const -> bool; + /** * Data role map that connects VideoSearchModel::DataRole to it's QML accessor name */ @@ -356,6 +376,9 @@ private: QString m_errorString = QString(); VideoSearchPaginator *m_paginator = nullptr; + VideoItemModel *m_itemModel = nullptr; + + qint64 m_lastCount = 0; Q_PROPERTY(State state READ state WRITE setState RESET resetState NOTIFY stateChanged FINAL) Q_PROPERTY(QString errorString READ errorString WRITE setErrorString NOTIFY errorStringChanged FINAL) @@ -366,6 +389,7 @@ private: 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_PROPERTY(VideoItemModel *itemModel READ itemModel CONSTANT FINAL) }; Q_DECLARE_METATYPE(VideoSearchModel)