File indexing completed on 2025-02-02 04:11:29

0001 /*
0002  * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best>
0003  *
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  */
0006 
0007 #include "lottiefiles_search_dialog.hpp"
0008 #include "ui_lottiefiles_search_dialog.h"
0009 #include <QEvent>
0010 #include <QJsonDocument>
0011 #include <QJsonObject>
0012 #include <QJsonArray>
0013 #include <QNetworkAccessManager>
0014 #include <QNetworkRequest>
0015 #include <QNetworkReply>
0016 #include <QImageReader>
0017 
0018 #include "app/log/log.hpp"
0019 #include "search_result.hpp"
0020 #include "graphql.hpp"
0021 
0022 
0023 class glaxnimate::gui::LottieFilesSearchDialog::Private
0024 {
0025 public:
0026     enum SearchType
0027     {
0028         Search,
0029         Featured
0030     };
0031 
0032     enum Direction
0033     {
0034         NewSearch,
0035         Next,
0036         Previous
0037     };
0038 
0039     struct Pagination
0040     {
0041         QString start;
0042         QString end;
0043 
0044         void next(QJsonObject& vars, int page_size) const
0045         {
0046             vars["first"] = page_size;
0047             vars["after"] = end;
0048             vars["before"] = QJsonValue::Null;
0049             vars["last"] = QJsonValue::Null;
0050         }
0051 
0052         void previous(QJsonObject& vars, int page_size) const
0053         {
0054             vars["first"] = QJsonValue::Null;
0055             vars["after"] = QJsonValue::Null;
0056             vars["before"] = start;
0057             vars["last"] = page_size;
0058         }
0059 
0060         void new_search(QJsonObject& vars, int page_size) const
0061         {
0062             vars["first"] = page_size;
0063             vars["after"] = QJsonValue::Null;
0064             vars["before"] = QJsonValue::Null;
0065             vars["last"] = QJsonValue::Null;
0066         }
0067 
0068         void vars(QJsonObject& vars, int page_size, Direction direction) const
0069         {
0070             switch ( direction )
0071             {
0072                 case NewSearch: return new_search(vars, page_size);
0073                 case Next: return next(vars, page_size);
0074                 case Previous: return previous(vars, page_size);
0075             }
0076         }
0077     };
0078 
0079     void search(SearchType type, Direction direction)
0080     {
0081         this->type = type;
0082 
0083         switch ( type )
0084         {
0085             case Search:
0086                 return search_query(current_query, direction);
0087             case Featured:
0088                 return featured(direction);
0089         }
0090     }
0091 
0092     void search_query(const QString& query, Direction direction)
0093     {
0094         static QString graphql_query = R"(
0095             query Search($before: String, $after: String, $first: Int, $last: Int, $query: String!)
0096             {
0097                 searchPublicAnimations(before: $before, after: $after, first: $first, last: $last, query: $query)
0098                 {
0099                     edges {
0100                         node {
0101                             bgColor,
0102                             id,
0103                             imageFrame,
0104                             imageUrl,
0105                             jsonUrl,
0106                             name,
0107                             url,
0108                             likesCount,
0109                             commentsCount,
0110                             createdBy {
0111                                 username
0112                             }
0113                         }
0114                     }
0115                     pageInfo {
0116                         startCursor
0117                         endCursor
0118                         hasNextPage
0119                         hasPreviousPage
0120                     }
0121                     totalCount
0122                 }
0123             })";
0124         QJsonObject vars;
0125         pagination.vars(vars, layout_rows * layout_columns, direction);
0126         vars["query"] = query;
0127 
0128         current_query = query;
0129         graphql.query(graphql_query, vars);
0130     }
0131 
0132     void featured(Direction direction)
0133     {
0134         static QString query = R"(
0135             query Search($before: String, $after: String, $first: Int, $last: Int, $collectionId: Float!)
0136             {
0137                 publicCollectionAnimations(before: $before, after: $after, first: $first, last: $last, collectionId: $collectionId)
0138                 {
0139                     edges {
0140                         node {
0141                             bgColor,
0142                             id,
0143                             imageFrame,
0144                             imageUrl,
0145                             jsonUrl,
0146                             name,
0147                             url,
0148                             likesCount,
0149                             commentsCount,
0150                             createdBy {
0151                                 username
0152                             }
0153                         }
0154                     }
0155                     pageInfo {
0156                         startCursor
0157                         endCursor
0158                         hasNextPage
0159                         hasPreviousPage
0160                     }
0161                     totalCount
0162                 }
0163             })";
0164         QJsonObject vars;
0165         pagination.vars(vars, layout_rows * layout_columns, direction);
0166         vars["collectionId"] = 1318984.;
0167 
0168         current_query = "";
0169 
0170         graphql.query(query, vars);
0171     }
0172 
0173     void on_response(QNetworkReply* reply)
0174     {
0175         end_load();
0176 
0177         auto response = QJsonDocument::fromJson(reply->readAll()).object();
0178 
0179         if ( response.contains("errors") )
0180         {
0181             for ( const auto& errv : response["errors"].toArray() )
0182             {
0183                 auto err = errv.toObject();
0184                 auto stream = app::log::Log("lottiefiles", "graphql").stream(app::log::Warning);
0185                 stream << err["message"].toString();
0186                 if ( err.contains("locations") )
0187                 {
0188                     for ( const auto& locv : err["locations"].toArray() )
0189                     {
0190                         auto loc = locv.toObject();
0191                         stream << " " << loc["line"].toInt() << ":" << loc["column"].toInt();
0192                     }
0193                 }
0194             }
0195         }
0196 
0197         auto d = response["data"].toObject();
0198 
0199         if ( !response["data"].isObject() || (!d["searchPublicAnimations"].isObject() && !d["publicCollectionAnimations"].isObject()) )
0200         {
0201             if ( reply->error() )
0202             {
0203                 auto code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
0204                 if ( code.isValid() )
0205                 {
0206                     auto reason = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
0207                     auto msg = i18n("HTTP Error %1: %2", code.toInt(), reason);
0208                     app::log::Log("lottiefiles", "http").stream(app::log::Warning) << reply->url().toString() << msg;
0209                     on_error(msg);
0210                 }
0211                 else
0212                 {
0213                     on_error(i18n("Network Error"));
0214                 }
0215             }
0216             else
0217             {
0218                 on_error("GraphQL Error");
0219             }
0220             return;
0221         }
0222 
0223         QJsonObject data;
0224         if ( d["publicCollectionAnimations"].isObject() )
0225             data = d["publicCollectionAnimations"].toObject();
0226         else
0227             data = d["searchPublicAnimations"].toObject();
0228 
0229         clear_results();
0230         auto qresults = data["edges"].toArray();
0231         results.reserve(qresults.size());
0232         for ( const auto& rev : qresults )
0233         {
0234             auto result = rev.toObject()["node"].toObject();
0235             add_result({
0236                 result["id"].toInt(),
0237                 result["name"].toString(),
0238                 result["createdBy"].toObject()["username"].toString(),
0239                 QUrl(result["url"].toString()),
0240                 QUrl(result["imageUrl"].toString()),
0241                 QUrl(result["jsonUrl"].toString()),
0242                 QColor(result["bgColor"].toString()),
0243                 result["likesCount"].toInt(),
0244                 result["commentsCount"].toInt(),
0245             });
0246         }
0247 
0248         auto page_info = data["pageInfo"].toObject();
0249 
0250         pagination.end = page_info["endCursor"].toString();
0251         pagination.start = page_info["startCursor"].toString();
0252 
0253         ui.button_next->setEnabled(page_info["hasNextPage"].toBool());
0254         ui.button_previous->setEnabled(page_info["hasPreviousPage"].toBool());
0255     }
0256 
0257     void on_error(const QString& msg)
0258     {
0259         ui.label_error->setVisible(true);
0260         ui.label_error->setText(msg);
0261     }
0262 
0263     void start_load()
0264     {
0265         ui.label_error->setVisible(false);
0266         ui.progress_bar->setMaximum(100);
0267         ui.progress_bar->setValue(0);
0268         ui.progress_bar->setVisible(true);
0269         ui.button_next->setEnabled(false);
0270     }
0271 
0272     void end_load()
0273     {
0274         ui.label_error->setVisible(false);
0275         ui.progress_bar->setVisible(false);
0276     }
0277 
0278     void clear_results()
0279     {
0280         ui.button_import->setEnabled(false);
0281         ui.button_open->setEnabled(false);
0282         results.clear();
0283     }
0284 
0285     void add_result(LottieFilesResult result)
0286     {
0287         std::size_t index = results.size();
0288         results.emplace_back(std::make_unique<LottieFilesResultItem>(std::move(result), dialog));
0289         auto widget = results.back().get();
0290         int row = index / layout_columns;
0291         int col = index % layout_columns;
0292         ui.result_area_layout->addWidget(widget, row, col);
0293         connect(widget, &LottieFilesResultItem::selected, dialog, [this](const QString& name, const QUrl& url) {
0294             on_selected(name, url);
0295         });
0296         connect(widget, &LottieFilesResultItem::selected_open, dialog, [this](const QString& name, const QUrl& url) {
0297             on_selected(name, url);
0298             dialog->done(Open);
0299         });
0300         connect(widget, &LottieFilesResultItem::selected_import, dialog, [this](const QString& name, const QUrl& url) {
0301             on_selected(name, url);
0302             dialog->done(Import);
0303         });
0304 
0305         auto reply = graphql.http().get(QNetworkRequest(widget->result().preview_url));
0306         connect(reply, &QNetworkReply::finished, widget, [widget, reply]{
0307             if ( reply->error() )
0308                 return;
0309 
0310             widget->set_preview_image(QImageReader(reply).read());
0311         });
0312     }
0313 
0314     void on_selected(const QString& name, const QUrl& url)
0315     {
0316         current_name = name;
0317         current_url = url;
0318         ui.button_import->setEnabled(true);
0319         ui.button_open->setEnabled(true);
0320     }
0321 
0322     void on_progress(qint64 bytes, qint64 total)
0323     {
0324         static constexpr const int maxi = std::numeric_limits<int>::max();
0325         if ( total > maxi )
0326         {
0327             bytes = (long double)(maxi) / total * bytes;
0328             total = maxi;
0329         }
0330         ui.progress_bar->setMaximum(total);
0331         ui.progress_bar->setValue(bytes);
0332     }
0333 
0334     Ui::LottieFilesSearchDialog ui;
0335     GraphQl graphql{"https://graphql.lottiefiles.com/2022-08/"};
0336     LottieFilesSearchDialog* dialog;
0337     QString current_query;
0338     std::vector<std::unique_ptr<LottieFilesResultItem>> results;
0339     int layout_columns = 4;
0340     int layout_rows = 3;
0341     QUrl current_url;
0342     QString current_name;
0343     Pagination pagination;
0344     SearchType type = SearchType::Featured;
0345 };
0346 
0347 glaxnimate::gui::LottieFilesSearchDialog::LottieFilesSearchDialog(QWidget* parent)
0348     : QDialog(parent), d(std::make_unique<Private>())
0349 {
0350     d->dialog = this;
0351     d->ui.setupUi(this);
0352     d->ui.progress_bar->setVisible(false);
0353     d->clear_results();
0354     d->ui.result_area->setMinimumWidth(80 * d->layout_columns + 128);
0355     d->featured(Private::NewSearch);
0356 
0357     connect(&d->graphql, &GraphQl::query_started, this, [this]{d->start_load();});
0358     connect(&d->graphql, &GraphQl::query_finished, this, [this](QNetworkReply* reply){d->on_response(reply);});
0359     connect(&d->graphql, &GraphQl::query_progress, this, [this](qint64 bytes, qint64 total){d->on_progress(bytes, total);});
0360 }
0361 
0362 glaxnimate::gui::LottieFilesSearchDialog::~LottieFilesSearchDialog() = default;
0363 
0364 void glaxnimate::gui::LottieFilesSearchDialog::changeEvent ( QEvent* e )
0365 {
0366     QDialog::changeEvent(e);
0367 
0368     if ( e->type() == QEvent::LanguageChange)
0369     {
0370         d->ui.retranslateUi(this);
0371     }
0372 }
0373 
0374 void glaxnimate::gui::LottieFilesSearchDialog::clicked_import()
0375 {
0376     done(Import);
0377 }
0378 
0379 void glaxnimate::gui::LottieFilesSearchDialog::clicked_open()
0380 {
0381     done(Open);
0382 }
0383 
0384 void glaxnimate::gui::LottieFilesSearchDialog::clicked_search()
0385 {
0386     if ( d->current_query != d->ui.input_query->text() )
0387     {
0388         d->current_query = d->ui.input_query->text();
0389         d->search(d->current_query.isEmpty() ? Private::Featured : Private::Search, Private::NewSearch);
0390     }
0391 }
0392 
0393 void glaxnimate::gui::LottieFilesSearchDialog::clicked_next()
0394 {
0395     d->search(d->type, Private::Next);
0396 }
0397 
0398 void glaxnimate::gui::LottieFilesSearchDialog::clicked_previous()
0399 {
0400     d->search(d->type, Private::Previous);
0401 }
0402 
0403 const QUrl & glaxnimate::gui::LottieFilesSearchDialog::selected_url() const
0404 {
0405     return d->current_url;
0406 }
0407 
0408 const QString & glaxnimate::gui::LottieFilesSearchDialog::selected_name() const
0409 {
0410     return d->current_name;
0411 }