File indexing completed on 2024-05-12 16:45:45
0001 /*************************************************************************** 0002 Copyright (C) 2019-2020 Robby Stephenson <robby@periapsis.org> 0003 ***************************************************************************/ 0004 0005 /*************************************************************************** 0006 * * 0007 * This program is free software; you can redistribute it and/or * 0008 * modify it under the terms of the GNU General Public License as * 0009 * published by the Free Software Foundation; either version 2 of * 0010 * the License or (at your option) version 3 or any later version * 0011 * accepted by the membership of KDE e.V. (or its successor approved * 0012 * by the membership of KDE e.V.), which shall act as a proxy * 0013 * defined in Section 14 of version 3 of the license. * 0014 * * 0015 * This program is distributed in the hope that it will be useful, * 0016 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0017 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0018 * GNU General Public License for more details. * 0019 * * 0020 * You should have received a copy of the GNU General Public License * 0021 * along with this program. If not, see <http://www.gnu.org/licenses/>. * 0022 * * 0023 ***************************************************************************/ 0024 0025 #include "colnectfetcher.h" 0026 #include "../collections/coincollection.h" 0027 #include "../collections/stampcollection.h" 0028 #include "../images/imagefactory.h" 0029 #include "../gui/combobox.h" 0030 #include "../utils/guiproxy.h" 0031 #include "../utils/string_utils.h" 0032 #include "../entry.h" 0033 #include "../fieldformat.h" 0034 #include "../core/filehandler.h" 0035 #include "../tellico_debug.h" 0036 0037 #include <KLocalizedString> 0038 #include <KConfigGroup> 0039 #include <KJob> 0040 #include <KJobUiDelegate> 0041 #include <KJobWidgets/KJobWidgets> 0042 #include <KIO/StoredTransferJob> 0043 0044 #include <QLabel> 0045 #include <QFile> 0046 #include <QTextStream> 0047 #include <QGridLayout> 0048 #include <QTextCodec> 0049 #include <QJsonDocument> 0050 #include <QJsonArray> 0051 #include <QJsonValue> 0052 #include <QRegularExpression> 0053 #include <QStandardPaths> 0054 0055 namespace { 0056 static const char* COLNECT_API_URL = "https://api.tellico-project.org/colnect"; 0057 // static const char* COLNECT_API_URL = "https://api.colnect.net"; 0058 static const char* COLNECT_IMAGE_URL = "https://i.colnect.net"; 0059 } 0060 0061 using namespace Tellico; 0062 using Tellico::Fetch::ColnectFetcher; 0063 0064 ColnectFetcher::ColnectFetcher(QObject* parent_) 0065 : Fetcher(parent_) 0066 , m_started(false) 0067 , m_locale(QStringLiteral("en")) { 0068 } 0069 0070 ColnectFetcher::~ColnectFetcher() { 0071 } 0072 0073 QString ColnectFetcher::source() const { 0074 return m_name.isEmpty() ? defaultName() : m_name; 0075 } 0076 0077 QString ColnectFetcher::attribution() const { 0078 return QStringLiteral("Catalog information courtesy of Colnect, an online collectors community."); 0079 } 0080 0081 bool ColnectFetcher::canSearch(Fetch::FetchKey k) const { 0082 return k == Title || k == Keyword; 0083 } 0084 0085 bool ColnectFetcher::canFetch(int type) const { 0086 return type == Data::Collection::Coin || type == Data::Collection::Stamp; 0087 } 0088 0089 void ColnectFetcher::readConfigHook(const KConfigGroup& config_) { 0090 QString k = config_.readEntry("Locale", "en"); 0091 if(!k.isEmpty()) { 0092 m_locale = k.toLower(); 0093 } 0094 Q_ASSERT_X(m_locale.length() == 2, "ColnectFetcher::readConfigHook", "lang should be 2 char short iso"); 0095 } 0096 0097 void ColnectFetcher::search() { 0098 m_started = true; 0099 m_year.clear(); 0100 0101 QUrl u(QString::fromLatin1(COLNECT_API_URL)); 0102 // Colnect API calls are encoded as a path 0103 QString query(QLatin1Char('/') + m_locale); 0104 0105 switch(collectionType()) { 0106 case Data::Collection::Coin: 0107 m_category = QStringLiteral("coins"); 0108 break; 0109 case Data::Collection::Stamp: 0110 m_category = QStringLiteral("stamps"); 0111 break; 0112 default: 0113 myWarning() << "Colnect category type not available for" << collectionType(); 0114 stop(); 0115 return; 0116 } 0117 0118 QString value = request().value(); 0119 switch(request().key()) { 0120 case Title: 0121 { 0122 query += QStringLiteral("/list/cat/") + m_category; 0123 // pull out year, keep the regexp a little loose 0124 QRegularExpression yearRX(QStringLiteral("[0-9]{4}")); 0125 QRegularExpressionMatch match = yearRX.match(value); 0126 if(match.hasMatch()) { 0127 m_year = match.captured(0); 0128 if(collectionType() == Data::Collection::Coin) { 0129 query += QStringLiteral("/mint_year/"); 0130 } else { 0131 query += QStringLiteral("/year/"); 0132 } 0133 query += m_year; 0134 value = value.remove(yearRX); 0135 } 0136 } 0137 // everything left is for the item description 0138 query += QStringLiteral("/item_name/") + value.simplified(); 0139 break; 0140 0141 case Keyword: 0142 { 0143 query += QStringLiteral("/list/cat/") + m_category; 0144 // pull out year, keep the regexp a little loose 0145 QRegularExpression yearRX(QStringLiteral("[0-9]{4}")); 0146 QRegularExpressionMatch match = yearRX.match(value); 0147 if(match.hasMatch()) { 0148 m_year = match.captured(0); 0149 if(collectionType() == Data::Collection::Coin) { 0150 query += QStringLiteral("/mint_year/"); 0151 } else { 0152 query += QStringLiteral("/year/"); 0153 } 0154 query += m_year; 0155 value = value.remove(yearRX); 0156 } 0157 } 0158 // everything left is for the item description 0159 query += QStringLiteral("/description/") + value.simplified(); 0160 break; 0161 0162 case Raw: 0163 query += QStringLiteral("/item/cat/") + m_category + QStringLiteral("/id/") + value; 0164 break; 0165 0166 default: 0167 myWarning() << "key not recognized:" << request().key(); 0168 stop(); 0169 return; 0170 } 0171 0172 u.setPath(u.path() + query); 0173 // myDebug() << "url:" << u; 0174 0175 m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); 0176 KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); 0177 connect(m_job.data(), &KJob::result, this, &ColnectFetcher::slotComplete); 0178 } 0179 0180 void ColnectFetcher::stop() { 0181 if(!m_started) { 0182 return; 0183 } 0184 if(m_job) { 0185 m_job->kill(); 0186 m_job = nullptr; 0187 } 0188 m_started = false; 0189 emit signalDone(this); 0190 } 0191 0192 Tellico::Data::EntryPtr ColnectFetcher::fetchEntryHook(uint uid_) { 0193 Data::EntryPtr entry = m_entries.value(uid_); 0194 if(!entry) { 0195 myWarning() << "no entry in dict"; 0196 return Data::EntryPtr(); 0197 } 0198 0199 // if there's a colnect-id in the entry, need to fetch all the data 0200 const QString id = entry->field(QStringLiteral("colnect-id")); 0201 if(!id.isEmpty()) { 0202 QUrl u(QString::fromLatin1(COLNECT_API_URL)); 0203 QString query(QLatin1Char('/') + m_locale + QStringLiteral("/item/cat/") 0204 + m_category + QStringLiteral("/id/") + id); 0205 u.setPath(u.path() + query); 0206 // myDebug() << "Reading item data from url:" << u; 0207 0208 QPointer<KIO::StoredTransferJob> job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); 0209 KJobWidgets::setWindow(job, GUI::Proxy::widget()); 0210 if(!job->exec()) { 0211 myDebug() << "Colnect item data:" << job->errorString() << u; 0212 return entry; 0213 } 0214 const QByteArray data = job->data(); 0215 if(data.isEmpty()) { 0216 myDebug() << "no colnect item data for" << u; 0217 return entry; 0218 } 0219 #if 0 0220 myWarning() << "Remove item debug from colnectfetcher.cpp"; 0221 QFile file(QStringLiteral("/tmp/colnectitemtest.json")); 0222 if(file.open(QIODevice::WriteOnly)) { 0223 QTextStream t(&file); 0224 t.setCodec("UTF-8"); 0225 t << data; 0226 } 0227 file.close(); 0228 #endif 0229 QJsonParseError jsonError; 0230 QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); 0231 Q_ASSERT_X(!doc.isNull(), "colnect", jsonError.errorString().toUtf8().constData()); 0232 const QVariantList resultList = doc.array().toVariantList(); 0233 Q_ASSERT_X(!resultList.isEmpty(), "colnect", "no item results"); 0234 Q_ASSERT_X(static_cast<QMetaType::Type>(resultList.at(0).type()) == QMetaType::QString, "colnect", 0235 "Weird single item result, first value is not a string"); 0236 populateEntry(entry, resultList); 0237 } 0238 0239 // image might still be a URL only 0240 loadImage(entry, QStringLiteral("obverse")); 0241 loadImage(entry, QStringLiteral("reverse")); 0242 loadImage(entry, QStringLiteral("image")); // stamp image 0243 0244 // don't want to include id 0245 entry->setField(QStringLiteral("colnect-id"), QString()); 0246 return entry; 0247 } 0248 0249 Tellico::Fetch::FetchRequest ColnectFetcher::updateRequest(Data::EntryPtr entry_) { 0250 const QString title = entry_->field(QStringLiteral("title")); 0251 if(!title.isEmpty()) { 0252 return FetchRequest(Keyword, title); 0253 } 0254 return FetchRequest(); 0255 } 0256 0257 void ColnectFetcher::slotComplete(KJob* job_) { 0258 KIO::StoredTransferJob* job = static_cast<KIO::StoredTransferJob*>(job_); 0259 0260 if(job->error()) { 0261 job->uiDelegate()->showErrorMessage(); 0262 stop(); 0263 return; 0264 } 0265 0266 const QByteArray data = job->data(); 0267 if(data.isEmpty()) { 0268 myDebug() << "no data"; 0269 stop(); 0270 return; 0271 } 0272 // see bug 319662. If fetcher is cancelled, job is killed 0273 // if the pointer is retained, it gets double-deleted 0274 m_job = nullptr; 0275 0276 #if 0 0277 myWarning() << "Remove debug from colnectfetcher.cpp"; 0278 QFile f(QStringLiteral("/tmp/colnecttest.json")); 0279 if(f.open(QIODevice::WriteOnly)) { 0280 QTextStream t(&f); 0281 t.setCodec("UTF-8"); 0282 t << data; 0283 } 0284 f.close(); 0285 #endif 0286 0287 QJsonParseError jsonError; 0288 QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); 0289 if(doc.isNull()) { 0290 myDebug() << "null JSON document:" << jsonError.errorString(); 0291 message(jsonError.errorString(), MessageHandler::Error); 0292 stop(); 0293 return; 0294 } 0295 QVariantList resultList = doc.array().toVariantList(); 0296 if(resultList.isEmpty()) { 0297 // myDebug() << "no results"; 0298 stop(); 0299 return; 0300 } 0301 0302 m_hasMoreResults = false; // for now, no continued searches 0303 0304 Data::CollPtr coll; 0305 if(collectionType() == Data::Collection::Coin) { 0306 coll = new Data::CoinCollection(true); 0307 } else { 0308 coll = new Data::StampCollection(true); 0309 } 0310 // placeholder for colnect id, to be removed later 0311 Data::FieldPtr f1(new Data::Field(QStringLiteral("colnect-id"), QString())); 0312 coll->addField(f1); 0313 0314 const QString series(QStringLiteral("series")); 0315 if(!coll->hasField(series) && optionalFields().contains(series)) { 0316 Data::FieldPtr field(new Data::Field(series, i18n("Series"))); 0317 field->setCategory(i18n("General")); 0318 coll->addField(field); 0319 } 0320 0321 const QString desc(QStringLiteral("description")); 0322 if(!coll->hasField(desc) && optionalFields().contains(desc)) { 0323 Data::FieldPtr field(new Data::Field(desc, i18n("Description"), Data::Field::Para)); 0324 coll->addField(field); 0325 } 0326 0327 const QString mintage(QStringLiteral("mintage")); 0328 if(!coll->hasField(mintage) && optionalFields().contains(mintage)) { 0329 Data::FieldPtr field(new Data::Field(mintage, i18n("Mintage"), Data::Field::Number)); 0330 field->setCategory(i18n("General")); 0331 coll->addField(field); 0332 } 0333 0334 const QString stanleygibbons(QStringLiteral("stanley-gibbons")); 0335 if(!coll->hasField(stanleygibbons) && optionalFields().contains(stanleygibbons)) { 0336 Data::FieldPtr field(new Data::Field(stanleygibbons, i18nc("Stanley Gibbons stamp catalog code", "Stanley Gibbons"))); 0337 field->setCategory(i18n("General")); 0338 coll->addField(field); 0339 } 0340 0341 const QString michel(QStringLiteral("michel")); 0342 if(!coll->hasField(michel) && optionalFields().contains(michel)) { 0343 Data::FieldPtr field(new Data::Field(michel, i18nc("Michel stamp catalog code", "Michel"))); 0344 field->setCategory(i18n("General")); 0345 coll->addField(field); 0346 } 0347 0348 // if the first item in the array is a string, probably a single item result, possibly from a Raw query 0349 if(!resultList.isEmpty() && 0350 static_cast<QMetaType::Type>(resultList.at(0).type()) == QMetaType::QString) { 0351 Data::EntryPtr entry(new Data::Entry(coll)); 0352 populateEntry(entry, resultList); 0353 0354 FetchResult* r = new FetchResult(this, entry); 0355 m_entries.insert(r->uid, entry); 0356 emit signalResultFound(r); 0357 0358 stop(); 0359 return; 0360 } 0361 0362 // here, we have multiple results to loop through 0363 // myDebug() << "Reading" << resultList.size() << "results"; 0364 foreach(const QVariant& result, resultList) { 0365 // be sure to check that the fetcher has not been stopped 0366 // crashes can occur if not 0367 if(!m_started) { 0368 break; 0369 } 0370 0371 Data::EntryPtr entry(new Data::Entry(coll)); 0372 //list action - returns array of [item_id,series_id,producer_id,front_picture_id, back_picture_id,item_description,catalog_codes,item_name] 0373 const QVariantList values = result.toJsonArray().toVariantList(); 0374 entry->setField(QStringLiteral("colnect-id"), values.first().toString()); 0375 if(optionalFields().contains(desc)) { 0376 entry->setField(desc, values.last().toString()); 0377 } 0378 entry->setField(QStringLiteral("year"), m_year); 0379 0380 FetchResult* r = new FetchResult(this, entry); 0381 m_entries.insert(r->uid, entry); 0382 emit signalResultFound(r); 0383 } 0384 0385 stop(); 0386 } 0387 0388 void ColnectFetcher::populateEntry(Data::EntryPtr entry_, const QVariantList& resultList_) { 0389 if(m_colnectFields.isEmpty()) { 0390 readDataList(); 0391 // set minimum size of list here 0392 if(m_colnectFields.count() < 26) { 0393 return; 0394 } 0395 } 0396 if(resultList_.count() != m_colnectFields.count()) { 0397 myDebug() << "field count mismatch! Got" << resultList_.count() << ", expected" << m_colnectFields.count(); 0398 return; 0399 } 0400 0401 // lookup the field name for the list index 0402 int idx = m_colnectFields.value(QStringLiteral("Issued on"), -1); 0403 // the year may have already been set in the query term 0404 if(m_year.isEmpty() && idx > -1) { 0405 entry_->setField(QStringLiteral("year"), resultList_.at(idx).toString()); 0406 } 0407 0408 idx = m_colnectFields.value(QStringLiteral("Country"), -1); 0409 if(idx > -1) { 0410 entry_->setField(QStringLiteral("country"), resultList_.at(idx).toString()); 0411 } 0412 0413 idx = m_colnectFields.value(QStringLiteral("Gum"), -1); 0414 if(idx > -1) { 0415 entry_->setField(QStringLiteral("gummed"), resultList_.at(idx).toString()); 0416 } 0417 0418 idx = m_colnectFields.value(QStringLiteral("Colors"), -1); 0419 if(idx > -1) { 0420 int colorId = resultList_.at(idx).toInt(); 0421 if(colorId > 0) { 0422 if(m_stampColors.isEmpty()) { 0423 readStampColors(); 0424 } 0425 entry_->setField(QStringLiteral("color"), m_stampColors.value(colorId)); 0426 } 0427 } 0428 0429 idx = m_colnectFields.value(QStringLiteral("Currency"), -1); 0430 if(idx > -1) { 0431 entry_->setField(QStringLiteral("currency"), resultList_.at(idx).toString()); 0432 idx = m_colnectFields.value(QStringLiteral("FaceValue"), -1); 0433 if(idx > -1) { 0434 // bad assumption, but go with it. First char is currency symbol 0435 QString currency = entry_->field(QStringLiteral("currency")); 0436 if(!currency.isEmpty()) currency.truncate(1); 0437 const double value = resultList_.at(idx).toDouble(); 0438 // don't assume the value is in system currency 0439 entry_->setField(QStringLiteral("denomination"), 0440 QLocale::system().toCurrencyString(value, currency)); 0441 } 0442 } 0443 0444 idx = m_colnectFields.value(QStringLiteral("Series"), -1); 0445 static const QString series(QStringLiteral("series")); 0446 if(idx > -1 && optionalFields().contains(series)) { 0447 entry_->setField(series, resultList_.at(idx).toString()); 0448 } 0449 0450 idx = m_colnectFields.value(QStringLiteral("Known mintage"), -1); 0451 static const QString mintage(QStringLiteral("mintage")); 0452 if(idx > -1 && optionalFields().contains(mintage)) { 0453 entry_->setField(mintage, resultList_.at(idx).toString()); 0454 } 0455 0456 idx = m_colnectFields.value(QStringLiteral("Description"), -1); 0457 static const QString desc(QStringLiteral("description")); 0458 if(idx > -1 && optionalFields().contains(desc)) { 0459 static const QString name(QStringLiteral("Name")); 0460 auto idxName = m_colnectFields.value(name, -1); 0461 QString s = resultList_.at(idx).toString().trimmed(); 0462 // use the name as the description for stamps since the title includes it 0463 // put the description text into the comments 0464 if(collectionType() == Data::Collection::Stamp) { 0465 if(idxName > -1) { 0466 entry_->setField(desc, resultList_.at(idxName).toString()); 0467 } 0468 entry_->setField(QStringLiteral("comments"), s); 0469 } else { 0470 // if description is empty, just use the name 0471 if(s.isEmpty() && idxName > -1) { 0472 entry_->setField(desc, resultList_.at(idxName).toString()); 0473 } else { 0474 entry_->setField(desc, s); 0475 } 0476 } 0477 } 0478 0479 // catalog codes 0480 idx = m_colnectFields.value(QStringLiteral("Catalog Codes"), -1); 0481 if(idx > -1) { 0482 // split by comma, look for prefix 0483 QStringList codes = resultList_.at(idx).toString().split(QLatin1Char(',')); 0484 Q_FOREACH(const QString& code, codes) { 0485 const QString prefix = code.section(QLatin1Char(':'), 0, 0).trimmed(); 0486 const QString value = code.section(QLatin1Char(':'), 1, 1).trimmed(); 0487 // 'SG' for Stanley Gibbons, 'Sc' for Scott, 'Mi' for Michel and 'Yv' for Yvert & Tellier. 0488 if(prefix == QLatin1String("Sc")) { 0489 entry_->setField(QStringLiteral("scott"), value); 0490 } else if(prefix == QLatin1String("Sg") && optionalFields().contains(QStringLiteral("stanley-gibbons"))) { 0491 entry_->setField(QStringLiteral("stanley-gibbons"), value); 0492 } else if(prefix == QLatin1String("Mi") && optionalFields().contains(QStringLiteral("michel"))) { 0493 entry_->setField(QStringLiteral("michel"), value); 0494 } 0495 } 0496 } 0497 0498 idx = m_colnectFields.value(QStringLiteral("FrontPicture"), -1); 0499 if(idx > -1) { 0500 // for coins, it's the obverse field. For stamps, it's just image 0501 if(collectionType() == Data::Collection::Coin && 0502 optionalFields().contains(QStringLiteral("obverse"))) { 0503 entry_->setField(QStringLiteral("obverse"), 0504 imageUrl(resultList_.at(0).toString(), 0505 resultList_.at(idx).toString())); 0506 } else if(collectionType() == Data::Collection::Stamp) { 0507 // always include the stamp image, no optional choice 0508 entry_->setField(QStringLiteral("image"), 0509 imageUrl(resultList_.at(0).toString(), 0510 resultList_.at(idx).toString())); 0511 } 0512 } 0513 0514 idx = m_colnectFields.value(QStringLiteral("BackPicture"), -1); 0515 if(idx > -1 && optionalFields().contains(QStringLiteral("reverse"))) { 0516 entry_->setField(QStringLiteral("reverse"), 0517 imageUrl(resultList_.at(0).toString(), 0518 resultList_.at(idx).toString())); 0519 } 0520 } 0521 0522 void ColnectFetcher::loadImage(Data::EntryPtr entry_, const QString& fieldName_) { 0523 const QString image = entry_->field(fieldName_); 0524 if(image.contains(QLatin1Char('/'))) { 0525 const QString id = ImageFactory::addImage(QUrl::fromUserInput(image), true /* quiet */); 0526 if(id.isEmpty()) { 0527 message(i18n("The cover image could not be loaded."), MessageHandler::Warning); 0528 } 0529 // empty image ID is ok 0530 entry_->setField(fieldName_, id); 0531 } 0532 } 0533 0534 Tellico::Fetch::ConfigWidget* ColnectFetcher::configWidget(QWidget* parent_) const { 0535 return new ColnectFetcher::ConfigWidget(parent_, this); 0536 } 0537 0538 QString ColnectFetcher::defaultName() { 0539 return QStringLiteral("Colnect"); // no translation 0540 } 0541 0542 QString ColnectFetcher::defaultIcon() { 0543 return favIcon("https://colnect.com"); 0544 } 0545 0546 Tellico::StringHash ColnectFetcher::allOptionalFields() { 0547 StringHash hash; 0548 // treat images as optional since Colnect doesn't break out different images for each year 0549 hash[QStringLiteral("obverse")] = i18n("Obverse"); 0550 hash[QStringLiteral("reverse")] = i18n("Reverse"); 0551 hash[QStringLiteral("series")] = i18n("Series"); 0552 /* TRANSLATORS: Mintage refers to the number of coins minted */ 0553 hash[QStringLiteral("mintage")] = i18n("Mintage"); 0554 hash[QStringLiteral("description")] = i18n("Description"); 0555 hash[QStringLiteral("stanley-gibbons")] = i18nc("Stanley Gibbons stamp catalog code", "Stanley Gibbons"); 0556 hash[QStringLiteral("michel")] = i18nc("Michel stamp catalog code", "Michel"); 0557 return hash; 0558 } 0559 0560 // Colnect specific method of turning name text into a slug 0561 // $str = html_entity_decode($str, ENT_QUOTES, 'UTF-8'); 0562 // $str = preg_replace('/&[^;]+;/', '_', $str); # change HTML elements to underscore 0563 // $str = str_replace(array('.', '"', '>', '<', '\\', ':', '/', '?', '#', '[', ']', '@', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '='), '', $str); 0564 // $str = preg_replace('/[\s_]+/', '_', $str); # any space sequence becomes a single underscore 0565 // $str = trim($str, '_'); # trim underscores 0566 QString ColnectFetcher::URLize(const QString& name_) { 0567 QString slug = name_; 0568 static const QString underscore(QStringLiteral("_")); 0569 static const QRegularExpression htmlElements(QStringLiteral("&[^;]+;")); 0570 static const QRegularExpression toRemove(QStringLiteral("[.\"><\\:/?#\\[\\]@!$&'()*+,;=]")); 0571 static const QRegularExpression spaces(QStringLiteral("\\s")); 0572 slug.replace(htmlElements, underscore); 0573 slug.remove(toRemove); 0574 slug.replace(spaces, underscore); 0575 while(slug.startsWith(underscore)) slug = slug.mid(1); 0576 while(slug.endsWith(underscore)) slug.chop(1); 0577 return slug; 0578 } 0579 0580 QString ColnectFetcher::imageUrl(const QString& name_, const QString& id_) { 0581 const QString nameSlug = URLize(name_); 0582 const int id = id_.toInt(); 0583 QUrl u(QString::fromLatin1(COLNECT_IMAGE_URL)); 0584 // uses 't' for thumbnail, use 'f' for full-size 0585 u.setPath(QString::fromLatin1("/t/%1/%2/%3.jpg") 0586 .arg(id / 1000) 0587 .arg(id % 1000, 3, 10, QLatin1Char('0')) 0588 .arg(nameSlug)); 0589 // myDebug() << "Image url:" << u; 0590 return u.toString(); 0591 } 0592 0593 void ColnectFetcher::readDataList() { 0594 // myDebug() << "Reading Colnect fields"; 0595 QUrl u(QString::fromLatin1(COLNECT_API_URL)); 0596 // Colnect API calls are encoded as a path 0597 QString query(QLatin1Char('/') + m_locale + QStringLiteral("/fields/cat/") + m_category + QLatin1Char('/')); 0598 u.setPath(u.path() + query); 0599 0600 // myDebug() << "Reading" << u; 0601 const QByteArray data = FileHandler::readDataFile(u, true); 0602 QJsonDocument doc = QJsonDocument::fromJson(data); 0603 if(doc.isNull()) { 0604 myDebug() << "null JSON document in colnect fields"; 0605 return; 0606 } 0607 QVariantList resultList = doc.array().toVariantList(); 0608 if(resultList.isEmpty()) { 0609 myDebug() << "no colnect field results"; 0610 return; 0611 } 0612 m_colnectFields.clear(); 0613 for(int i = 0; i < resultList.size(); ++i) { 0614 m_colnectFields.insert(resultList.at(i).toString(), i); 0615 // if(i == 5) myDebug() << m_colnectFields; 0616 } 0617 // myDebug() << "Number of Colnect fields:" << m_colnectFields.count(); 0618 } 0619 0620 void ColnectFetcher::readStampColors() { 0621 QUrl u(QString::fromLatin1(COLNECT_API_URL)); 0622 // Colnect API calls are encoded as a path 0623 QString query(QLatin1Char('/') + m_locale + QStringLiteral("/colors/cat/") + m_category + QLatin1Char('/')); 0624 u.setPath(u.path() + query); 0625 0626 // myDebug() << "Reading stamp colors from" << u; 0627 const QByteArray data = FileHandler::readDataFile(u, true); 0628 QJsonDocument doc = QJsonDocument::fromJson(data); 0629 if(doc.isNull()) { 0630 myDebug() << "null JSON document in colnect fields"; 0631 return; 0632 } 0633 QJsonArray resultList = doc.array(); 0634 if(resultList.isEmpty()) { 0635 myDebug() << "no stamp color results"; 0636 return; 0637 } 0638 m_stampColors.clear(); 0639 for(int i = 0; i < resultList.size(); ++i) { 0640 // an array of arrays, first value is id, second is color name 0641 const QJsonArray values = resultList.at(i).toArray(); 0642 if(values.size() > 2) { 0643 m_stampColors.insert(values.at(0).toInt(), values.at(1).toString()); 0644 // if(i == 5) myDebug() << resultList.at(i) << m_stampColors; 0645 } 0646 } 0647 // myDebug() << "Number of stamp colors:" << m_stampColors.count(); 0648 } 0649 0650 ColnectFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const ColnectFetcher* fetcher_) 0651 : Fetch::ConfigWidget(parent_) { 0652 QGridLayout* l = new QGridLayout(optionsWidget()); 0653 l->setSpacing(4); 0654 l->setColumnStretch(1, 10); 0655 0656 int row = -1; 0657 0658 QLabel* label = new QLabel(i18n("Language: "), optionsWidget()); 0659 l->addWidget(label, ++row, 0); 0660 m_langCombo = new GUI::ComboBox(optionsWidget()); 0661 0662 #define LANG_ITEM(NAME, CY, ISO) \ 0663 m_langCombo->addItem(QIcon(QStandardPaths::locate(QStandardPaths::GenericDataLocation, \ 0664 QStringLiteral("kf5/locale/countries/" CY "/flag.png"))), \ 0665 i18nc("Language", NAME), \ 0666 QLatin1String(ISO)); 0667 LANG_ITEM("English", "us", "en"); 0668 LANG_ITEM("French", "fr", "fr"); 0669 LANG_ITEM("German", "de", "de"); 0670 LANG_ITEM("Spanish", "es", "es"); 0671 #undef LANG_ITEM 0672 0673 // instead of trying to include all possible languages offered by Colnect 0674 // allow the user to enter it 0675 m_langCombo->setEditable(true); 0676 QRegularExpression rx(QLatin1String("\\w\\w")); // only 2 characters 0677 QRegularExpressionValidator* val = new QRegularExpressionValidator(rx, this); 0678 m_langCombo->setValidator(val); 0679 0680 void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated; 0681 connect(m_langCombo, activatedInt, this, &ConfigWidget::slotSetModified); 0682 connect(m_langCombo, activatedInt, this, &ConfigWidget::slotLangChanged); 0683 l->addWidget(m_langCombo, row, 1); 0684 label->setBuddy(m_langCombo); 0685 0686 l->setRowStretch(++row, 10); 0687 0688 // now add additional fields widget 0689 addFieldsWidget(ColnectFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); 0690 0691 if(fetcher_) { 0692 bool success = m_langCombo->setCurrentData(fetcher_->m_locale); 0693 // a user-entered iso code might not be in the data list, insert it if not 0694 if(!success) { 0695 m_langCombo->addItem(fetcher_->m_locale, fetcher_->m_locale); 0696 m_langCombo->setCurrentIndex(m_langCombo->count()-1); 0697 } 0698 } 0699 } 0700 0701 void ColnectFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { 0702 QString lang = m_langCombo->currentData().toString(); 0703 if(lang.isEmpty()) { 0704 // might be user-entered 0705 lang = m_langCombo->currentText(); 0706 } 0707 config_.writeEntry("Locale", lang); 0708 } 0709 0710 QString ColnectFetcher::ConfigWidget::preferredName() const { 0711 return QString::fromLatin1("Colnect (%1)").arg(m_langCombo->currentText()); 0712 } 0713 0714 void ColnectFetcher::ConfigWidget::slotLangChanged() { 0715 emit signalName(preferredName()); 0716 }