1205 lines
37 KiB
C++
1205 lines
37 KiB
C++
#include "KomplexSearchModel.h"
|
|
#include <qeventloop.h>
|
|
|
|
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<DataRoles>(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<int, QByteArray> KomplexSearchModel::roleNames() const
|
|
{
|
|
return m_dataRoles;
|
|
}
|
|
|
|
void KomplexSearchModel::downloadMedia(QString fileLocation, QString fileUrl)
|
|
{
|
|
QUrl remoteUrl(QStringLiteral("https://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<QString,QString> 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<QJsonObject> 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(QString::fromUtf8(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<quint64>(infoObject[QStringLiteral("flags")].toInt()),
|
|
static_cast<bool>(infoObject[QStringLiteral("hasLiked")].toInt()),
|
|
infoObject[QStringLiteral("id")].toString(),
|
|
static_cast<quint64>(infoObject[QStringLiteral("likes")].toInt()),
|
|
infoObject[QStringLiteral("name")].toString(),
|
|
static_cast<quint64>(infoObject[QStringLiteral("published")].toInt()),
|
|
tags,
|
|
static_cast<bool>(infoObject[QStringLiteral("usePreview")].toInt()),
|
|
infoObject[QStringLiteral("username")].toString(),
|
|
rootObject[QStringLiteral("ver")].toString(),
|
|
static_cast<quint64>(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<ShaderToyRenderInput> inputs;
|
|
QList<ShaderToyRenderOutput> outputs;
|
|
|
|
for(const QJsonValue &inputValue : std::as_const(inputArray))
|
|
{
|
|
QJsonObject samplerObject = inputValue[QStringLiteral("sampler")].toObject();
|
|
inputs.append
|
|
(
|
|
ShaderToyRenderInput
|
|
{
|
|
static_cast<quint8>(inputValue[QStringLiteral("channel")].toInt()),
|
|
inputValue[QStringLiteral("ctype")].toString(),
|
|
samplerObject[QStringLiteral("filter")].toString(),
|
|
static_cast<quint64>(inputValue[QStringLiteral("id")].toInt()),
|
|
samplerObject[QStringLiteral("internal")].toString(),
|
|
static_cast<bool>(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<quint8>(outputValue[QStringLiteral("channel")].toInt()),
|
|
static_cast<quint64>(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<DataRoles>(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();
|
|
}
|