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 }