From 9303dbf589d39d897c6d7506144565444725e0da Mon Sep 17 00:00:00 2001 From: Digital Artifex <7929434+DigitalArtifex@users.noreply.github.com> Date: Tue, 4 Nov 2025 05:24:26 -0500 Subject: [PATCH] Added custom API backend to replace ShaderToy. Fixes #4 --- package/contents/ui/ShaderToyHub.qml | 32 +- plugin/CMakeLists.txt | 4 + plugin/KomplexSearchModel.cpp | 1204 ++++++++++++++++++++++++++ plugin/KomplexSearchModel.h | 274 ++++++ plugin/plugin.cpp | 2 + plugin/plugin.json | 8 +- plugin/qmldir | 3 +- 7 files changed, 1512 insertions(+), 15 deletions(-) create mode 100644 plugin/KomplexSearchModel.cpp create mode 100644 plugin/KomplexSearchModel.h diff --git a/package/contents/ui/ShaderToyHub.qml b/package/contents/ui/ShaderToyHub.qml index 9e14c35..228d36d 100644 --- a/package/contents/ui/ShaderToyHub.qml +++ b/package/contents/ui/ShaderToyHub.qml @@ -16,7 +16,7 @@ Item signal accepted - Komplex.ShaderToySearchModel + Komplex.KomplexSearchModel { id: searchModel } @@ -174,6 +174,18 @@ Item } } + RowLayout + { + width: thumbnailImage.width + + Text + { + color: palette.text + anchors.fill: parent + text: model.name + } + } + RowLayout { visible: parent.itemIndex === view.currentIndex @@ -245,7 +257,7 @@ Item onClicked: () => { workingThumbnail.source = model.thumbnail - searchModel.convert(model.index); + searchModel.download(model.index); downloadDialog.close(); } } @@ -333,7 +345,7 @@ Item width: mainItem.width height: mainItem.height - visible: searchModel.status === Komplex.ShaderToySearchModel.Searching || searchModel.status === Komplex.ShaderToySearchModel.Compiling + visible: searchModel.status === Komplex.KomplexSearchModel.Searching || searchModel.status === Komplex.KomplexSearchModel.Compiling RowLayout { @@ -341,7 +353,7 @@ Item Image { - visible: searchModel.status === Komplex.ShaderToySearchModel.Compiling + visible: searchModel.status === Komplex.KomplexSearchModel.Compiling Layout.fillHeight: true Layout.fillWidth: true @@ -355,7 +367,7 @@ Item text: searchModel.statusMessage color: palette.text elide: Text.ElideRight - visible: searchModel.status === Komplex.ShaderToySearchModel.Compiling + visible: searchModel.status === Komplex.KomplexSearchModel.Compiling } ProgressBar @@ -363,7 +375,7 @@ Item id: totalProgress Layout.fillWidth: true Layout.preferredHeight: 6 - visible: searchModel.status === Komplex.ShaderToySearchModel.Compiling + visible: searchModel.status === Komplex.KomplexSearchModel.Compiling } Text @@ -372,7 +384,7 @@ Item text: qsTr(searchModel.downloadText) color: palette.text elide: Text.ElideRight - visible: searchModel.totalDownloads > 0 && searchModel.status === Komplex.ShaderToySearchModel.Compiling + visible: searchModel.totalDownloads > 0 && searchModel.status === Komplex.KomplexSearchModel.Compiling } ProgressBar @@ -383,7 +395,7 @@ Item from: 0 to: searchModel.totalDownloads value: searchModel.completedDownloads - visible: searchModel.totalDownloads > 0 && searchModel.status === Komplex.ShaderToySearchModel.Compiling + visible: searchModel.totalDownloads > 0 && searchModel.status === Komplex.KomplexSearchModel.Compiling } } @@ -419,7 +431,7 @@ Item function onStatusChanged() { - if(searchModel.status === Komplex.ShaderToySearchModel.Compiled) + if(searchModel.status === Komplex.KomplexSearchModel.Compiled) mediaSelectionItem.open() } } @@ -586,7 +598,7 @@ Item { console.log("Search Model Status " + searchModel.status) - if(searchModel.status === Komplex.ShaderToySearchModel.Error) + if(searchModel.status === Komplex.KomplexSearchModel.Error) { warningDialog.open(); } diff --git a/plugin/CMakeLists.txt b/plugin/CMakeLists.txt index 9f67830..0947efa 100644 --- a/plugin/CMakeLists.txt +++ b/plugin/CMakeLists.txt @@ -33,6 +33,8 @@ add_library( ShaderToySearchModel.cpp ShaderToyAPI.h PexelsAPI.h + KomplexSearchModel.h + KomplexSearchModel.cpp ) qt_add_qml_module( @@ -66,6 +68,8 @@ qt_add_qml_module( ShaderToySearchModel.cpp ShaderToyAPI.h PexelsAPI.h + KomplexSearchModel.h + KomplexSearchModel.cpp NO_GENERATE_PLUGIN_SOURCE ) diff --git a/plugin/KomplexSearchModel.cpp b/plugin/KomplexSearchModel.cpp new file mode 100644 index 0000000..7b85ea8 --- /dev/null +++ b/plugin/KomplexSearchModel.cpp @@ -0,0 +1,1204 @@ +#include "KomplexSearchModel.h" +#include + +KomplexSearchModel::KomplexSearchModel(QObject *parent) + : QAbstractItemModel{parent} +{ + m_networkManager.setAutoDeleteReplies(true); +} + +QVariant KomplexSearchModel::data(const QModelIndex &index, int role) const +{ + if(index.row() < 0 || index.row() >= m_data.count()) + return QVariant(); + + QVariant data; + + switch (static_cast(role)) + { + case Date: + data = QVariant::fromValue(m_data[index.row()].metadata.date); + break; + case Description: + data = QVariant::fromValue(m_data[index.row()].metadata.description); + break; + case EmbedUrl: + data = QVariant::fromValue(QStringLiteral("https://www.shadertoy.com/embed/%1").arg(m_data[index.row()].metadata.id)); + break; + case Flags: + data = QVariant::fromValue(m_data[index.row()].metadata.flags); + break; + case HasLiked: + data = QVariant::fromValue(m_data[index.row()].metadata.hasLiked); + break; + case Id: + data = QVariant::fromValue(m_data[index.row()].metadata.id); + break; + case Likes: + data = QVariant::fromValue(m_data[index.row()].metadata.likes); + break; + case Name: + data = QVariant::fromValue(m_data[index.row()].metadata.name); + break; + case Published: + data = QVariant::fromValue(m_data[index.row()].metadata.published); + break; + case Tags: + data = QVariant::fromValue(m_data[index.row()].metadata.tags); + break; + case Thumbnail: + data = QVariant::fromValue(QStringLiteral("https://www.shadertoy.com/media/shaders/%1.jpg").arg(m_data[index.row()].metadata.id)); + break; + case UsePreview: + data = QVariant::fromValue(m_data[index.row()].metadata.usePreview); + break; + case Username: + data = QVariant::fromValue(m_data[index.row()].metadata.username); + break; + case Version: + data = QVariant::fromValue(m_data[index.row()].metadata.version); + break; + case Views: + data = QVariant::fromValue(m_data[index.row()].metadata.views); + break; + case State: + data = QVariant::fromValue(m_data[index.row()].status); + break; + } + + return data; +} + +QHash KomplexSearchModel::roleNames() const +{ + return m_dataRoles; +} + +void KomplexSearchModel::downloadMedia(QString fileLocation, QString fileUrl) +{ + QUrl remoteUrl(QStringLiteral("http://api.artifex.services/v1%2").arg(fileUrl)); + QNetworkRequest request(remoteUrl); + QNetworkReply *reply = m_manager.get(request); + + QEventLoop loop; + + QObject::connect + ( + reply, + &QNetworkReply::finished, + this, + [this, reply, fileLocation, fileUrl]() + { + if(reply->error()) + { + qWarning() << reply->errorString(); + setDownloadText(reply->errorString()); + return; + } + + QByteArray headerData = reply->rawHeader(QStringLiteral("Content-Type")); + + if(!headerData.isValidUtf8()) + { + qWarning() << QStringLiteral("Header data is not valid UTF8 data"); + return; + } + + QString type = QString::fromUtf8(headerData); + + if(!type.startsWith(QStringLiteral("image/"))) + { + qWarning() << QStringLiteral("Downloaded content is not an image").arg(type.toUpper()); + setDownloadText(QStringLiteral("Downloaded content is not an image").arg(type.toUpper())); + return; + } + + QFile file(fileLocation); + QByteArray data = reply->readAll(); + + if(!file.open(QFile::ReadWrite)) + { + qWarning() << QStringLiteral("Could not open file to download").arg(type.toUpper()); + setDownloadText(QStringLiteral("Could not open file to download").arg(type.toUpper())); + return; + } + + if(!file.write(data) != data.length()) + { + file.close(); + qWarning() << QStringLiteral("Could not write file to download").arg(type.toUpper()); + setDownloadText(QStringLiteral("Could not write file to download").arg(type.toUpper())); + return; + } + + file.close(); + + // This is causing errors on JPG format, but JPEG is fine + + // type.remove(QStringLiteral("image/")); + + // QPixmap pixmap; + // pixmap.loadFromData(reply->readAll(), type.toUpper().toStdString().c_str()); + + // if(pixmap.isNull()) + // { + // qWarning() << QStringLiteral("Media format (%1) is not supported").arg(type.toUpper()); + // setDownloadText(QStringLiteral("Media format (%1) is not supported").arg(type.toUpper())); + // return; + // } + + // pixmap.save(fileLocation, type.toUpper().toStdString().c_str()); + + setDownloadText(QStringLiteral("Downloaded %1").arg(fileUrl)); + setCompletedDownloads(completedDownloads() + 1); + } + ); +} + +void KomplexSearchModel::compile(quint64 index) +{ + setStatus(Compiling, QStringLiteral("Compiling Shader")); + + ShaderToyEntry entry = m_data[index]; + + QDir localToolsDirectory(QStringLiteral("%1/.local/share/komplex/tools").arg(QStandardPaths::writableLocation(QStandardPaths::HomeLocation))); + QString inputDirectory = QStringLiteral("%1/komplex/src/%2").arg(QStandardPaths::writableLocation(QStandardPaths::TempLocation), entry.metadata.id); + QString outputDirectory = QStringLiteral("%1/komplex/build").arg(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); + QString shaderPackDirectory = QStringLiteral("%1/komplex/build/%2").arg(QStandardPaths::writableLocation(QStandardPaths::TempLocation), entry.metadata.id); + + QStringList arguments = + { + localToolsDirectory.absoluteFilePath(QStringLiteral("stc.py")), + QStringLiteral("-i"), + inputDirectory, + QStringLiteral("-o"), + outputDirectory + }; + + if(!QFile::exists(localToolsDirectory.absoluteFilePath(QStringLiteral("stc.py")))) + { + setStatus(Error, QStringLiteral("Shader Compiler is not installed at %1").arg(localToolsDirectory.absoluteFilePath(QStringLiteral("stc.py")))); + return; + } + + QProcess *process = new QProcess(this); + + 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 + QString::fromUtf8(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; + } + + setCompilerErrorOutput(m_compilerOutput + QString::fromUtf8(processData)); + } + ); + + process->start(QStringLiteral("python3"), arguments); + + if(!process->waitForStarted(3000)) + { + qWarning() << process->readAll(); + setStatus(Error, QStringLiteral("Could not start shader compiler")); + + process->deleteLater(); + return; + } + + if(!process->waitForFinished()) + { + qWarning() << process->readAll(); + setStatus(Error, QStringLiteral("Shader compiler timeout")); + + process->deleteLater(); + return; + } + + if(process->exitCode() != 0) + { + qWarning() << process->readAll(); + setStatus(Error, QStringLiteral("Shader compiler error")); + + process->deleteLater(); + return; + } + + qWarning() << process->readAll(); + process->deleteLater(); +} + +void KomplexSearchModel::save(quint64 index) +{ + ShaderToyEntry entry = m_data[index]; + + setStatus(Compiling, QStringLiteral("Saving shader data")); + setCompletedDownloads(0); + setTotalDownloads(0); + + 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]); +} + +void KomplexSearchModel::install(quint64 index) +{ + ShaderToyEntry entry = m_data[index]; + + setStatus(Finalizing, QStringLiteral("Installing Shader")); + setCompletedDownloads(0); + setTotalDownloads(0); + + QString tempLocation = QStringLiteral("%1/komplex/build/%2").arg(QStandardPaths::writableLocation(QStandardPaths::TempLocation), entry.metadata.id); + QString installLocation = QStringLiteral("%1/.local/share/komplex/packs/%2").arg(QStandardPaths::writableLocation(QStandardPaths::HomeLocation), entry.metadata.id); + + 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 + QString::fromUtf8(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; + } + + setCompilerErrorOutput(m_compilerOutput + QString::fromUtf8(processData)); + } + ); + + QStringList arguments = QStringList { QStringLiteral("-R"), tempLocation, installLocation}; + process.start(QStringLiteral("cp"), arguments); + + if(!process.waitForStarted(3000)) + { + qWarning() << QStringLiteral("Could not start copy process: %1").arg(process.readAllStandardError()); + setStatus(Error, QStringLiteral("Could not start install process")); + return; + } + + if(!process.waitForFinished()) + { + qWarning() << QStringLiteral("Copy process took longer than expected (>30s)"); + setStatus(Error, QStringLiteral("Install process took longer than expected (>30s)")); + return; + } +} + +void KomplexSearchModel::download(quint64 index) +{ + ShaderToyEntry entry = m_data[index]; + entry.status = ShaderToyEntry::Loading; + m_data[index] = entry; + + QModelIndex modelIndex = this->index(index, 0); + Q_EMIT dataChanged(modelIndex, modelIndex); + + setStatus(Loading, QStringLiteral("Downloading Metadata")); + QString id = entry.metadata.id; + + QUrl url(QStringLiteral("https://api.artifex.services/v1/shaders/item/%1").arg(entry.metadata.id)); + QNetworkReply *reply = m_manager.get(QNetworkRequest(url)); + + QObject::connect + ( + reply, + &QNetworkReply::finished, + this, + [this, reply, index, id] + { + if(reply->error()) + { + setStatus(Error, QStringLiteral("Network Error %1:\n %2\n %3").arg(id, reply->errorString(), QStringLiteral("https://api.komplex.services/v1/shaders/item/"))); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError jsonError; + + QJsonDocument document = QJsonDocument::fromJson(data, &jsonError); + + if(jsonError.error != QJsonParseError::NoError) + { + qWarning() << jsonError.errorString(); + setStatus(Error, jsonError.errorString()); + return; + } + + 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 = m_data[index]; + + 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() + } + ); + } + + entry.status = ShaderToyEntry::Idle; + + m_data.replace(index, entry); + + QModelIndex modelIndex = this->index(index, 0); + Q_EMIT dataChanged(modelIndex, modelIndex); + + convert(index); + } + ); +} + +void KomplexSearchModel::resetModel() +{ + beginResetModel(); + m_data.clear(); + endResetModel(); +} + +KomplexSearchModel::Status KomplexSearchModel::status() const +{ + return m_status; +} + +void KomplexSearchModel::setStatus(const Status &status, const QString &message) +{ + setStatusMessage(message); + + if (m_status == status) + return; + + if(status == Error && !message.isEmpty()) + qWarning() << message; + + m_status = status; + Q_EMIT statusChanged(); +} + +QModelIndex KomplexSearchModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return createIndex(row, column, &m_data.at(row)); +} + +int KomplexSearchModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 0; +} + +QModelIndex KomplexSearchModel::parent(const QModelIndex &index) const +{ + Q_UNUSED(index) + return QModelIndex(); +} + +bool KomplexSearchModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if(index.row() < 0 || index.row() >= m_data.count()) + return false; + + ShaderToyEntry entry = m_data[index.row()]; + + switch (static_cast(role)) + { + case Date: + entry.metadata.date = value.toDateTime(); + break; + case Description: + entry.metadata.description = value.toString(); + break; + case EmbedUrl: + break; + case Flags: + entry.metadata.flags = value.toInt(); + break; + case HasLiked: + entry.metadata.hasLiked = value.toBool(); + break; + case Id: + entry.metadata.id = value.toString(); + break; + case Likes: + entry.metadata.likes = value.toInt(); + break; + case Name: + entry.metadata.name = value.toString(); + break; + case Published: + entry.metadata.published = value.toBool(); + break; + case Tags: + entry.metadata.tags = value.toStringList(); + break; + case Thumbnail: + break; + case UsePreview: + entry.metadata.usePreview = value.toBool(); + break; + case Username: + entry.metadata.username = value.toString(); + break; + case Version: + entry.metadata.version = value.toString(); + break; + case Views: + entry.metadata.views = value.toInt(); + break; + case State: + break; + } + + beginInsertRows(QModelIndex(), index.row(), index.row()); + m_data.replace(index.row(), entry); + endInsertRows(); + + return true; +} + +QString KomplexSearchModel::lastSavedFile() const +{ + return m_lastSavedFile; +} + +void KomplexSearchModel::setLastSavedFile(const QString &lastSavedFile) +{ + if (m_lastSavedFile == lastSavedFile) + return; + + m_lastSavedFile = lastSavedFile; + Q_EMIT lastSavedFileChanged(); +} + +QStringList KomplexSearchModel::videoSelections() const +{ + return m_videoSelections; +} + +void KomplexSearchModel::setVideoSelections(const QStringList &videoSelections) +{ + if (m_videoSelections == videoSelections) + return; + m_videoSelections = videoSelections; + Q_EMIT videoSelectionsChanged(); +} + +QString KomplexSearchModel::statusMessage() const +{ + return m_statusMessage; +} + +void KomplexSearchModel::setStatusMessage(const QString &statusMessage) +{ + if (m_statusMessage == statusMessage) + return; + + m_statusMessage = statusMessage; + Q_EMIT statusMessageChanged(); +} + +QString KomplexSearchModel::downloadText() const +{ + return m_downloadText; +} + +void KomplexSearchModel::setDownloadText(const QString &downloadText) +{ + if (m_downloadText == downloadText) + return; + m_downloadText = downloadText; + Q_EMIT downloadTextChanged(); +} + +quint64 KomplexSearchModel::completedDownloads() const +{ + return m_completedDownloads; +} + +void KomplexSearchModel::setCompletedDownloads(quint64 completedDownloads) +{ + if (m_completedDownloads == completedDownloads) + return; + m_completedDownloads = completedDownloads; + Q_EMIT completedDownloadsChanged(); +} + +quint64 KomplexSearchModel::totalDownloads() const +{ + return m_totalDownloads; +} + +void KomplexSearchModel::setTotalDownloads(quint64 totalDownloads) +{ + if (m_totalDownloads == totalDownloads) + return; + m_totalDownloads = totalDownloads; + Q_EMIT totalDownloadsChanged(); +} + +QString KomplexSearchModel::compilerErrorOutput() const +{ + return m_compilerErrorOutput; +} + +void KomplexSearchModel::setCompilerErrorOutput(const QString &compilerErrorOutput) +{ + if (m_compilerErrorOutput == compilerErrorOutput) + return; + m_compilerErrorOutput = compilerErrorOutput; + Q_EMIT compilerErrorOutputChanged(); +} + +QString KomplexSearchModel::compilerOutput() const +{ + return m_compilerOutput; +} + +void KomplexSearchModel::setCompilerOutput(const QString &compilerOutput) +{ + if (m_compilerOutput == compilerOutput) + return; + m_compilerOutput = compilerOutput; + Q_EMIT compilerOutputChanged(); +} + +quint64 KomplexSearchModel::totalPages() const +{ + return m_totalPages; +} + +void KomplexSearchModel::setTotalPages(quint64 totalPages) +{ + if (m_totalPages == totalPages) + return; + + m_totalPages = totalPages; + Q_EMIT totalPagesChanged(); +} + +void KomplexSearchModel::next() +{ + if(m_currentPage >= m_totalPages) + return; + + setCurrentPage(m_currentPage + 1); + setQuery(m_query); +} + +void KomplexSearchModel::previous() +{ + if(m_currentPage <= 0) + return; + + setCurrentPage(m_currentPage - 1); + setQuery(m_query); +} + +void KomplexSearchModel::convert(qsizetype index) +{ + if(index < 0 || index >= m_data.count()) + return; + + setVideoSelections(QStringList()); + + save(index); + + if(status() == Error) + return; + + compile(index); + + if(status() == Error) + return; + + if(videoSelections().count() > 0) + setStatus(Compiled, QStringLiteral("Shader Compiled")); + else + finalize(index); +} + +ShaderToyEntry KomplexSearchModel::entry(qsizetype index) +{ + if(index < 0 || index >= m_data.count()) + return ShaderToyEntry(); + + return m_data[index]; +} + +void KomplexSearchModel::finalize(qsizetype index) +{ + setStatus(Finalizing, QStringLiteral("Finalizing Shader")); + install(index); + setStatus(Idle, QStringLiteral("Shader Installed")); + setVideoSelections(QStringList()); + + Q_EMIT shaderInstalled(); +} + +void KomplexSearchModel::replaceSource(qsizetype index, QString uuid, QString source) +{ + if(index < 0 || index >= m_data.count() || m_data.count() == 0) + return; + + QString tempLocation = QStringLiteral("%1/komplex/build/%2").arg(QStandardPaths::writableLocation(QStandardPaths::TempLocation), m_data[index].metadata.id); + + QFile sourceFile(source); + QFileInfo sourceInfo(source); + + if(!sourceFile.exists()) + { + setStatus(Error, QStringLiteral("Source file does not exist")); + return; + } + + if(QFile::exists(QStringLiteral("%1/videos/%2").arg(tempLocation, sourceInfo.fileName()))) + QFile::remove(QStringLiteral("%1/videos/%2").arg(tempLocation, sourceInfo.fileName())); + + if(!sourceFile.copy(QStringLiteral("%1/videos/%2").arg(tempLocation, sourceInfo.fileName()))) + { + qWarning() << sourceFile.errorString(); + setStatus(Error, QStringLiteral("Source file could not be copied to temp directory")); + return; + } + + QFile packFile(QStringLiteral("%1/pack.json").arg(tempLocation)); + + if(!packFile.exists()) + { + setStatus(Error, QStringLiteral("Pack file could not be located")); + return; + } + + if(!packFile.open(QFile::ReadOnly)) + { + setStatus(Error, QStringLiteral("Could not open pack file")); + return; + } + + QString sourceName = QStringLiteral("./videos/%1").arg(sourceInfo.fileName()); + + QByteArray packData = packFile.readAll(); + packData.replace(uuid.toLatin1(), sourceName.toLatin1()); + + packFile.close(); + + if(!packFile.open(QFile::WriteOnly)) + { + setStatus(Error, QStringLiteral("Could not open pack file")); + return; + } + + if(packFile.write(packData) != packData.length()) + { + setStatus(Error, QStringLiteral("Could not write to pack file. File may be corrupted")); + return; + } + + packFile.close(); + + m_selectionMutex.lock(); + QStringList newSelections = m_videoSelections; + newSelections.removeAll(uuid); + setVideoSelections(newSelections); + + if(videoSelections().count() == 0) + finalize(index); + + m_selectionMutex.unlock(); +} + +quint64 KomplexSearchModel::currentPage() const +{ + return m_currentPage; +} + +void KomplexSearchModel::setCurrentPage(quint64 currentPage) +{ + if (m_currentPage == currentPage) + return; + + m_currentPage = currentPage; + Q_EMIT currentPageChanged(); +} + +quint64 KomplexSearchModel::resultsPerPage() const +{ + return m_resultsPerPage; +} + +void KomplexSearchModel::setResultsPerPage(quint64 resultsPerPage) +{ + if (m_resultsPerPage == resultsPerPage) + return; + + m_resultsPerPage = resultsPerPage; + Q_EMIT resultsPerPageChanged(); + + setTotalPages(m_totalResults / m_resultsPerPage); + setQuery(m_query); +} + +QString KomplexSearchModel::query() const +{ + return m_query; +} + +void KomplexSearchModel::setQuery(const QString &query) +{ + m_query = query; + Q_EMIT queryChanged(); + + if(m_currentPage == 0) + setCurrentPage(1); + + getSearchResults(QStringLiteral("https://api.artifex.services/v1/shaders/search/%1/%2/%3").arg(QUrl::toPercentEncoding(m_query)).arg((m_currentPage - 1) * m_resultsPerPage).arg(m_resultsPerPage)); +} + +void KomplexSearchModel::getSearchResults(QString url) +{ + setStatus(Searching, QStringLiteral("Loading Query \"%1\"").arg(m_query)); + + resetModel(); + + QNetworkRequest request; + request.setUrl(QUrl(url)); + + QNetworkReply *reply = m_networkManager.get(request); + + QObject::connect + ( + reply, + &QNetworkReply::finished, + this, + [this, reply]() + { + if(reply->error()) + qWarning() << reply->errorString(); + + QByteArray data = reply->readAll(); + QJsonParseError jsonError; + + QJsonDocument document = QJsonDocument::fromJson(data, &jsonError); + + if(jsonError.error != QJsonParseError::NoError) + { + qWarning() << jsonError.errorString(); + setStatus(Error, jsonError.errorString()); + return; + } + + QJsonObject rootObject = document.object(); + setTotalResults(rootObject[QStringLiteral("total_results")].toInt()); + + QJsonArray results = rootObject[QStringLiteral("results")].toArray(); + beginInsertRows(QModelIndex(), 0, results.count() - 1); + + for(const QJsonValue &value : std::as_const(results)) + { + if(!value.isObject()) + continue; + + QJsonObject entryObject = value.toObject(); + + // add mostly default entry to be filled out async + ShaderToyEntry entry; + entry.metadata = ShaderToyMetadata + { + QDateTime::currentDateTime(), + entryObject.value(QStringLiteral("description")).toString(), + 0, + false, + entryObject.value(QStringLiteral("id")).toString(), + 0, + entryObject.value(QStringLiteral("name")).toString(), + 0, + entryObject.value(QStringLiteral("tags")).toVariant().toStringList(), + false, + entryObject.value(QStringLiteral("username")).toString(), + QString(), + 0 + }; + m_data.append(entry); + //download(m_data.count() - 1); + } + + endInsertRows(); + setStatus(Idle, QString()); + } + ); +} + +quint64 KomplexSearchModel::totalResults() const +{ + return m_totalResults; +} + +void KomplexSearchModel::setTotalResults(quint64 totalResults) +{ + if (m_totalResults == totalResults) + return; + + m_totalResults = totalResults; + Q_EMIT totalResultsChanged(); + + setTotalPages(m_totalResults / m_resultsPerPage); +} + +int KomplexSearchModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_data.size(); +} \ No newline at end of file diff --git a/plugin/KomplexSearchModel.h b/plugin/KomplexSearchModel.h new file mode 100644 index 0000000..d27c6e0 --- /dev/null +++ b/plugin/KomplexSearchModel.h @@ -0,0 +1,274 @@ +#ifndef KomplexSearchModel_H +#define KomplexSearchModel_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ShaderToyMetadata.h" +#include "Komplex_global.h" + +class KOMPLEX_EXPORT KomplexSearchModel : public QAbstractItemModel +{ + Q_OBJECT +public: + + enum DataRoles + { + Date = Qt::UserRole + 1, + Description, + EmbedUrl, + Flags, + HasLiked, + Id, + Likes, + Name, + Published, + State, + Tags, + Thumbnail, + UsePreview, + Username, + Version, + Views + }; + Q_ENUM(DataRoles) + + enum Status + { + Idle, + Loading, + Searching, + Compiling, + Compiled, + Finalizing, + Error + }; + Q_ENUM(Status) + + KomplexSearchModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + + virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + + QString lastSavedFile() const; + void setLastSavedFile(const QString &lastSavedFile); + + Status status() const; + void setStatus(const Status &status, const QString &message = QString()); + + QString query() const; + void setQuery(const QString &query); + + quint64 resultsPerPage() const; + void setResultsPerPage(quint64 resultsPerPage); + + quint64 totalResults() const; + void setTotalResults(quint64 totalResults); + + quint64 currentPage() const; + void setCurrentPage(quint64 currentPage); + + quint64 totalPages() const; + void setTotalPages(quint64 totalPages); + + Q_INVOKABLE void next(); + Q_INVOKABLE void previous(); + Q_INVOKABLE void convert(qsizetype index); + Q_INVOKABLE ShaderToyEntry entry(qsizetype index); + Q_INVOKABLE void finalize(qsizetype index); + Q_INVOKABLE void replaceSource(qsizetype index, QString uuid, QString source); + + QString compilerOutput() const; + void setCompilerOutput(const QString &compilerOutput); + + QString compilerErrorOutput() const; + void setCompilerErrorOutput(const QString &compilerErrorOutput); + + quint64 totalDownloads() const; + void setTotalDownloads(quint64 totalDownloads); + + quint64 completedDownloads() const; + void setCompletedDownloads(quint64 completedDownloads); + + QString downloadText() const; + void setDownloadText(const QString &downloadText); + + QString statusMessage() const; + void setStatusMessage(const QString &statusMessage); + + QStringList videoSelections() const; + void setVideoSelections(const QStringList &videoSelections); + Q_INVOKABLE void download(quint64 index); + +Q_SIGNALS: + void shaderInstalled(); + void lastSavedFileChanged(); + void statusChanged(); + void queryChanged(); + void resultsPerPageChanged(); + void totalResultsChanged(); + void currentPageChanged(); + void totalPagesChanged(); + void compilerOutputChanged(); + void compilerErrorOutputChanged(); + void totalDownloadsChanged(); + void completedDownloadsChanged(); + void downloadTextChanged(); + void statusMessageChanged(); + void videoSelectionsChanged(); + +protected: + QHash roleNames() const override; + +private: + void downloadMedia(QString fileLocation, QString fileUrl); + void compile(quint64 index); + void save(quint64 index); + void install(quint64 index); + void resetModel(); + void getSearchResults(QString url); + + quint64 getFileSize(QUrl url); + QString sizeText(quint64 size); + + QNetworkAccessManager m_networkManager; + QString m_query; + + QList m_data; + QString m_lastSavedFile; + qreal m_downloadProgress = 0; + Status m_status = Status::Idle; + + quint64 m_resultsPerPage = 12; + quint64 m_totalResults = 0; + quint64 m_currentPage = 0; + quint64 m_totalPages = 0; + + QString m_compilerOutput; + QString m_compilerErrorOutput; + quint64 m_completedDownloads = 0; + quint64 m_totalDownloads = 0; + QString m_downloadText; + QString m_statusMessage; + + QStringList m_videoSelections; + + QNetworkAccessManager m_manager; + + // multiple possible connections to replaceSource + QMutex m_selectionMutex; + + static inline const QHash m_dataRoles = + { + { + static_cast(Date), + QByteArray("date") + }, + { + static_cast(Description), + QByteArray("description") + }, + { + static_cast(EmbedUrl), + QByteArray("embedUrl") + }, + { + static_cast(Flags), + QByteArray("flags") + }, + { + static_cast(HasLiked), + QByteArray("hasLiked") + }, + { + static_cast(Id), + QByteArray("id") + }, + { + static_cast(Likes), + QByteArray("likes") + }, + { + static_cast(Name), + QByteArray("name") + }, + { + static_cast(Published), + QByteArray("published") + }, + { + static_cast(State), + QByteArray("state") + }, + { + static_cast(Tags), + QByteArray("tags") + }, + { + static_cast(Thumbnail), + QByteArray("thumbnail") + }, + { + static_cast(UsePreview), + QByteArray("usePreview") + }, + { + static_cast(Username), + QByteArray("username") + }, + { + static_cast(Version), + QByteArray("version") + }, + { + static_cast(Views), + QByteArray("views") + } + }; + + const static inline QStringList m_supportedChannelTypes = + { + QStringLiteral("buffer"), + QStringLiteral("image"), + QStringLiteral("video"), + QStringLiteral("audio"), + QStringLiteral("texture") + }; + + Q_PROPERTY(QString lastSavedFile READ lastSavedFile WRITE setLastSavedFile NOTIFY lastSavedFileChanged FINAL) + Q_PROPERTY(Status status READ status WRITE setStatus NOTIFY statusChanged FINAL) + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged FINAL) + Q_PROPERTY(quint64 resultsPerPage READ resultsPerPage WRITE setResultsPerPage NOTIFY resultsPerPageChanged FINAL) + Q_PROPERTY(quint64 totalResults READ totalResults WRITE setTotalResults NOTIFY totalResultsChanged FINAL) + Q_PROPERTY(quint64 currentPage READ currentPage WRITE setCurrentPage NOTIFY currentPageChanged FINAL) + Q_PROPERTY(quint64 totalPages READ totalPages WRITE setTotalPages NOTIFY totalPagesChanged FINAL) + Q_PROPERTY(QString compilerOutput READ compilerOutput WRITE setCompilerOutput NOTIFY compilerOutputChanged FINAL) + Q_PROPERTY(QString compilerErrorOutput READ compilerErrorOutput WRITE setCompilerErrorOutput NOTIFY compilerErrorOutputChanged FINAL) + Q_PROPERTY(quint64 totalDownloads READ totalDownloads WRITE setTotalDownloads NOTIFY totalDownloadsChanged FINAL) + Q_PROPERTY(quint64 completedDownloads READ completedDownloads WRITE setCompletedDownloads NOTIFY completedDownloadsChanged FINAL) + Q_PROPERTY(QString downloadText READ downloadText WRITE setDownloadText NOTIFY downloadTextChanged FINAL) + Q_PROPERTY(QString statusMessage READ statusMessage WRITE setStatusMessage NOTIFY statusMessageChanged FINAL) + Q_PROPERTY(QStringList videoSelections READ videoSelections WRITE setVideoSelections NOTIFY videoSelectionsChanged FINAL) +}; +Q_DECLARE_METATYPE(KomplexSearchModel) +#endif // KomplexSearchModel_H + \ No newline at end of file diff --git a/plugin/plugin.cpp b/plugin/plugin.cpp index cc2f870..6dc1738 100644 --- a/plugin/plugin.cpp +++ b/plugin/plugin.cpp @@ -9,6 +9,7 @@ #include "PexelsImageSearch.h" #include "ShaderToySearchModel.h" #include "GeometryProvider.h" +#include "KomplexSearchModel.h" #include "Komplex_global.h" AudioModel *komplexAudioSingletonProvider(QQmlEngine *engine, QJSEngine *scriptEngine) @@ -47,6 +48,7 @@ public: qmlRegisterType(uri, 1, 0, "ShaderToySearchModel"); qmlRegisterType(uri, 1, 0, "PexelsVideoSearchModel"); qmlRegisterType(uri, 1, 0, "PexelsImageSearchModel"); + qmlRegisterType(uri, 1, 0, "KomplexSearchModel"); } void unregisterTypes() override diff --git a/plugin/plugin.json b/plugin/plugin.json index c555782..3ed3b17 100644 --- a/plugin/plugin.json +++ b/plugin/plugin.json @@ -1,8 +1,8 @@ { "Id" : "komplex", "Name" : "Komplex Wallpaper Plugin", - "Version" : "1.0.0", - "CompatVersion" : "1.0.0", + "Version" : "1.0.7", + "CompatVersion" : "1.0.7", "VendorId" : "digitalartifex", "Vendor" : "Digital Artifex", "Copyright" : "(C) 2025 Digital Artifex", @@ -13,6 +13,6 @@ "Description" : [ "This plugin enables functionality for the Komplex wallpaper, allowing users to set and manage complex shader arrangements as wallpapers in their KDE environment." ], - "Url" : "http://www.github.com/digitalartifex/komplex", - "DocumentationUrl" : "http://www.github.com/digitalartifex/komplex/wiki" + "Url" : "https://github.com/DigitalArtifex/kde-komplex-wallpaper-engine", + "DocumentationUrl" : "https://github.com/DigitalArtifex/kde-komplex-wallpaper-engine/wiki" } \ No newline at end of file diff --git a/plugin/qmldir b/plugin/qmldir index 9b54180..c5a6dc0 100644 --- a/plugin/qmldir +++ b/plugin/qmldir @@ -4,4 +4,5 @@ classname AudioModel classname ShaderPackModel classname PexelsImageSearchModel classname PexelsVideoSearchModel -classname ShaderToySearchModel \ No newline at end of file +classname ShaderToySearchModel +classname KomplexSearchModel \ No newline at end of file