1 /***************************************************************************
2 Copyright (C) 2012-2019 Robby Stephenson <robby@periapsis.org>
3 ***************************************************************************/
4
5 /***************************************************************************
6 * *
7 * This program is free software; you can redistribute it and/or *
8 * modify it under the terms of the GNU General Public License as *
9 * published by the Free Software Foundation; either version 2 of *
10 * the License or (at your option) version 3 or any later version *
11 * accepted by the membership of KDE e.V. (or its successor approved *
12 * by the membership of KDE e.V.), which shall act as a proxy *
13 * defined in Section 14 of version 3 of the license. *
14 * *
15 * This program is distributed in the hope that it will be useful, *
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
18 * GNU General Public License for more details. *
19 * *
20 * You should have received a copy of the GNU General Public License *
21 * along with this program. If not, see <http://www.gnu.org/licenses/>. *
22 * *
23 ***************************************************************************/
24
25 #include "thegamesdbfetcher.h"
26 #include "../collections/gamecollection.h"
27 #include "../images/imagefactory.h"
28 #include "../gui/combobox.h"
29 #include "../core/filehandler.h"
30 #include "../utils/guiproxy.h"
31 #include "../utils/string_utils.h"
32 #include "../utils/tellico_utils.h"
33 #include "../tellico_debug.h"
34
35 #include <KLocalizedString>
36 #include <KConfigGroup>
37 #include <KJob>
38 #include <KJobUiDelegate>
39 #include <KJobWidgets/KJobWidgets>
40 #include <KIO/StoredTransferJob>
41
42 #include <QLabel>
43 #include <QLineEdit>
44 #include <QFile>
45 #include <QTextStream>
46 #include <QGridLayout>
47 #include <QTextCodec>
48 #include <QJsonDocument>
49 #include <QJsonArray>
50 #include <QJsonObject>
51 #include <QUrlQuery>
52 #include <QTimer>
53
54 namespace {
55 static const int THEGAMESDB_MAX_RETURNS_TOTAL = 20;
56 static const char* THEGAMESDB_API_URL = "https://api.thegamesdb.net";
57 static const char* THEGAMESDB_API_VERSION = "1"; // krazy:exclude=doublequote_chars
58 static const char* THEGAMESDB_MAGIC_TOKEN = "f7c4fd9c5d6d4a2fcefe3157192f87e260038abe86b0f3977716596edaebdbb82315586e98fc88b0fb9ff4c01576e4d47b4e556d487a4325221abbddfac36f59d7e114753b5fa6c77a1e73423d5f72460f3b526bcbae4f2be0d86a5854600436784e3a5c5d6bc1a3e2d395f798fb35073051f2c232014023e9dda99edfea5767";
59 }
60
61 using namespace Tellico;
62 using Tellico::Fetch::TheGamesDBFetcher;
63
TheGamesDBFetcher(QObject * parent_)64 TheGamesDBFetcher::TheGamesDBFetcher(QObject* parent_)
65 : Fetcher(parent_)
66 , m_started(false)
67 , m_imageSize(SmallImage) {
68 m_apiKey = Tellico::reverseObfuscate(THEGAMESDB_MAGIC_TOKEN);
69 // delay reading the platform names from the cache file
70 QTimer::singleShot(0, this, &TheGamesDBFetcher::loadCachedData);
71 }
72
~TheGamesDBFetcher()73 TheGamesDBFetcher::~TheGamesDBFetcher() {
74 }
75
source() const76 QString TheGamesDBFetcher::source() const {
77 return m_name.isEmpty() ? defaultName() : m_name;
78 }
79
canSearch(Fetch::FetchKey k) const80 bool TheGamesDBFetcher::canSearch(Fetch::FetchKey k) const {
81 return k == Title;
82 }
83
canFetch(int type) const84 bool TheGamesDBFetcher::canFetch(int type) const {
85 return type == Data::Collection::Game;
86 }
87
readConfigHook(const KConfigGroup & config_)88 void TheGamesDBFetcher::readConfigHook(const KConfigGroup& config_) {
89 const QString k = config_.readEntry("API Key");
90 if(!k.isEmpty()) {
91 m_apiKey = k;
92 }
93 const int imageSize = config_.readEntry("Image Size", -1);
94 if(imageSize > -1) {
95 m_imageSize = static_cast<ImageSize>(imageSize);
96 }
97 }
98
search()99 void TheGamesDBFetcher::search() {
100 m_started = true;
101
102 if(m_apiKey.isEmpty()) {
103 myDebug() << "empty API key";
104 message(i18n("An access key is required to use this data source.")
105 + QLatin1Char(' ') +
106 i18n("Those values must be entered in the data source settings."), MessageHandler::Error);
107 stop();
108 return;
109 }
110
111 QUrl u(QString::fromLatin1(THEGAMESDB_API_URL));
112 u.setPath(QLatin1String("/v") + QLatin1String(THEGAMESDB_API_VERSION));
113
114 switch(request().key()) {
115 case Title:
116 u = u.adjusted(QUrl::StripTrailingSlash);
117 u.setPath(u.path() + QLatin1String("/Games/ByGameName"));
118 {
119 QUrlQuery q;
120 q.addQueryItem(QStringLiteral("apikey"), m_apiKey);
121 if(optionalFields().contains(QStringLiteral("num-player"))) {
122 q.addQueryItem(QStringLiteral("fields"), QStringLiteral("players,rating,publishers,genres,overview,platform"));
123 } else {
124 q.addQueryItem(QStringLiteral("fields"), QStringLiteral("rating,publishers,genres,overview,platform"));
125 }
126 q.addQueryItem(QStringLiteral("include"), QStringLiteral("platform,boxart"));
127 q.addQueryItem(QStringLiteral("name"), request().value());
128 if(!request().data().isEmpty()) {
129 q.addQueryItem(QStringLiteral("filter[platform]"), request().data());
130 }
131 u.setQuery(q);
132 }
133 break;
134
135 default:
136 myWarning() << "key not recognized:" << request().key();
137 stop();
138 return;
139 }
140 // u = QUrl::fromLocalFile(QStringLiteral("/tmp/test-tgdb.json"));
141 // myDebug() << u;
142
143 m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo);
144 KJobWidgets::setWindow(m_job, GUI::Proxy::widget());
145 connect(m_job.data(), &KJob::result, this, &TheGamesDBFetcher::slotComplete);
146 }
147
stop()148 void TheGamesDBFetcher::stop() {
149 if(!m_started) {
150 return;
151 }
152 if(m_job) {
153 m_job->kill();
154 m_job = nullptr;
155 }
156 m_started = false;
157 emit signalDone(this);
158 }
159
fetchEntryHook(uint uid_)160 Tellico::Data::EntryPtr TheGamesDBFetcher::fetchEntryHook(uint uid_) {
161 Data::EntryPtr entry = m_entries.value(uid_);
162 if(!entry) {
163 myWarning() << "no entry in dict";
164 return Data::EntryPtr();
165 }
166
167 // image might still be a URL
168 const QString image_id = entry->field(QStringLiteral("cover"));
169 if(image_id.contains(QLatin1Char('/'))) {
170 const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true /* quiet */);
171 if(id.isEmpty()) {
172 message(i18n("The cover image could not be loaded."), MessageHandler::Warning);
173 }
174 // empty image ID is ok
175 entry->setField(QStringLiteral("cover"), id);
176 }
177
178 // don't want to include TGDb ID field
179 entry->setField(QStringLiteral("tgdb-id"), QString());
180
181 return entry;
182 }
183
updateRequest(Data::EntryPtr entry_)184 Tellico::Fetch::FetchRequest TheGamesDBFetcher::updateRequest(Data::EntryPtr entry_) {
185 const QString platform = entry_->field(QStringLiteral("platform"));
186 int platformId = -1;
187 // if the platform id is available, it can be used to filter the update search
188 if(!platform.isEmpty()) {
189 for(auto i = m_platforms.constBegin(); i != m_platforms.constEnd(); ++i) {
190 if(i.value() == platform) {
191 platformId = i.key();
192 break;
193 }
194 }
195 }
196
197 const QString title = entry_->field(QStringLiteral("title"));
198 if(!title.isEmpty()) {
199 FetchRequest req(Title, title);
200 if(platformId > -1) {
201 req.setData(QString::number(platformId));
202 }
203 return req;
204 }
205 return FetchRequest();
206 }
207
slotComplete(KJob * job_)208 void TheGamesDBFetcher::slotComplete(KJob* job_) {
209 KIO::StoredTransferJob* job = static_cast<KIO::StoredTransferJob*>(job_);
210
211 if(job->error()) {
212 job->uiDelegate()->showErrorMessage();
213 stop();
214 return;
215 }
216
217 const QByteArray data = job->data();
218 if(data.isEmpty()) {
219 myDebug() << "no data";
220 stop();
221 return;
222 }
223 // see bug 319662. If fetcher is cancelled, job is killed
224 // if the pointer is retained, it gets double-deleted
225 m_job = nullptr;
226
227 #if 0
228 myWarning() << "Remove debug from thegamesdbfetcher.cpp";
229 QFile f(QStringLiteral("/tmp/test-tgdb.json"));
230 if(f.open(QIODevice::WriteOnly)) {
231 QTextStream t(&f);
232 t.setCodec("UTF-8");
233 t << data;
234 }
235 f.close();
236 #endif
237
238 Data::CollPtr coll(new Data::GameCollection(true));
239 // always add the tgdb-id for fetchEntryHook
240 Data::FieldPtr field(new Data::Field(QStringLiteral("tgdb-id"), QStringLiteral("TGDb ID"), Data::Field::Line));
241 field->setCategory(i18n("General"));
242 coll->addField(field);
243
244 if(optionalFields().contains(QStringLiteral("num-player"))) {
245 Data::FieldPtr field(new Data::Field(QStringLiteral("num-player"), i18n("Number of Players"), Data::Field::Number));
246 field->setCategory(i18n("General"));
247 field->setFlags(Data::Field::AllowMultiple | Data::Field::AllowGrouped);
248 coll->addField(field);
249 }
250
251 QVariantMap topLevelMap = QJsonDocument::fromJson(data).object().toVariantMap();
252 if(!topLevelMap.contains(QStringLiteral("data"))) {
253 myDebug() << "No data in result!";
254 }
255 readPlatformList(topLevelMap.value(QStringLiteral("include")).toMap()
256 .value(QStringLiteral("platform")).toMap());
257 readCoverList(topLevelMap.value(QStringLiteral("include")).toMap()
258 .value(QStringLiteral("boxart")).toMap());
259
260 QVariantList resultList = topLevelMap.value(QStringLiteral("data")).toMap()
261 .value(QStringLiteral("games")).toList();
262 if(resultList.isEmpty()) {
263 myDebug() << "no results";
264 stop();
265 return;
266 }
267
268 int count = 0;
269 foreach(const QVariant& result, resultList) {
270 Data::EntryPtr entry(new Data::Entry(coll));
271 populateEntry(entry, result.toMap());
272
273 FetchResult* r = new FetchResult(this, entry);
274 m_entries.insert(r->uid, entry);
275 emit signalResultFound(r);
276 ++count;
277 if(count >= THEGAMESDB_MAX_RETURNS_TOTAL) {
278 break;
279 }
280 }
281
282 stop();
283 }
284
populateEntry(Data::EntryPtr entry_,const QVariantMap & resultMap_)285 void TheGamesDBFetcher::populateEntry(Data::EntryPtr entry_, const QVariantMap& resultMap_) {
286 entry_->setField(QStringLiteral("tgdb-id"), mapValue(resultMap_, "id"));
287 entry_->setField(QStringLiteral("title"), mapValue(resultMap_, "game_title"));
288 entry_->setField(QStringLiteral("year"), mapValue(resultMap_, "release_date").left(4));
289 entry_->setField(QStringLiteral("description"), mapValue(resultMap_, "overview"));
290
291 const int platformId = mapValue(resultMap_, "platform").toInt();
292 if(m_platforms.contains(platformId)) {
293 const QString platform = m_platforms[platformId];
294 // make the assumption that if the platform name isn't already in the allowed list, it should be added
295 Data::FieldPtr f = entry_->collection()->fieldByName(QStringLiteral("platform"));
296 if(f && !f->allowed().contains(platform)) {
297 f->setAllowed(QStringList(f->allowed()) << platform);
298 }
299 entry_->setField(QStringLiteral("platform"), platform);
300 }
301
302 const QString esrb = mapValue(resultMap_, "rating")
303 .section(QLatin1Char('-'), 1, 1)
304 .trimmed(); // value is like "T - Teen"
305 if(!esrb.isEmpty()) {
306 entry_->setField(QStringLiteral("certification"), i18n(esrb.toUtf8().constData()));
307 }
308 const QString coverUrl = m_covers.value(mapValue(resultMap_, "id"));
309 if(m_imageSize != NoImage) {
310 entry_->setField(QStringLiteral("cover"), coverUrl);
311 }
312
313 QStringList genres, pubs, devs;
314
315 bool alreadyAttemptedLoad = false;
316 QVariantList genreIdList = resultMap_.value(QStringLiteral("genres")).toList();
317 foreach(const QVariant& v, genreIdList) {
318 const int id = v.toInt();
319 if(!m_genres.contains(id) && !alreadyAttemptedLoad) {
320 readDataList(Genre);
321 alreadyAttemptedLoad = true;
322 }
323 if(m_genres.contains(id)) {
324 genres << m_genres[id];
325 }
326 }
327
328 alreadyAttemptedLoad = false;
329 QVariantList pubList = resultMap_.value(QStringLiteral("publishers")).toList();
330 foreach(const QVariant& v, pubList) {
331 const int id = v.toInt();
332 if(!m_publishers.contains(id) && !alreadyAttemptedLoad) {
333 readDataList(Publisher);
334 alreadyAttemptedLoad = true;
335 }
336 if(m_publishers.contains(id)) {
337 pubs << m_publishers[id];
338 }
339 }
340
341 alreadyAttemptedLoad = false;
342 QVariantList devList = resultMap_.value(QStringLiteral("developers")).toList();
343 foreach(const QVariant& v, devList) {
344 const int id = v.toInt();
345 if(!m_developers.contains(id) && !alreadyAttemptedLoad) {
346 readDataList(Developer);
347 alreadyAttemptedLoad = true;
348 }
349 if(m_developers.contains(id)) {
350 devs << m_developers[id];
351 }
352 }
353
354 entry_->setField(QStringLiteral("genre"), genres.join(FieldFormat::delimiterString()));
355 entry_->setField(QStringLiteral("publisher"), pubs.join(FieldFormat::delimiterString()));
356 entry_->setField(QStringLiteral("developer"), devs.join(FieldFormat::delimiterString()));
357
358 if(entry_->collection()->hasField(QStringLiteral("num-player"))) {
359 entry_->setField(QStringLiteral("num-player"), mapValue(resultMap_, "players"));
360 }
361 }
362
readPlatformList(const QVariantMap & platformMap_)363 void TheGamesDBFetcher::readPlatformList(const QVariantMap& platformMap_) {
364 QMapIterator<QString, QVariant> i(platformMap_);
365 while(i.hasNext()) {
366 i.next();
367 const QVariantMap map = i.value().toMap();
368 const QString name = map.value(QStringLiteral("name")).toString();
369 m_platforms.insert(i.key().toInt(), Data::GameCollection::normalizePlatform(name));
370 }
371
372 // now write it to cache again
373 const QString id = QStringLiteral("id");
374 const QString name = QStringLiteral("name");
375 QJsonObject platformObj;
376 for(auto ii = m_platforms.constBegin(); ii != m_platforms.constEnd(); ++ii) {
377 QJsonObject iObj;
378 iObj.insert(id, ii.key());
379 iObj.insert(name, ii.value());
380 platformObj.insert(QString::number(ii.key()), iObj);
381 }
382 QJsonObject dataObj;
383 dataObj.insert(QStringLiteral("platforms"), platformObj);
384 QJsonObject docObj;
385 docObj.insert(QStringLiteral("data"), dataObj);
386 QJsonDocument doc;
387 doc.setObject(docObj);
388 writeDataList(Platform, doc.toJson());
389 }
390
readCoverList(const QVariantMap & coverDataMap_)391 void TheGamesDBFetcher::readCoverList(const QVariantMap& coverDataMap_) {
392 // first, get the base url
393 QString imageBase;
394 switch(m_imageSize) {
395 case SmallImage:
396 // this is the default size, using the thumb. Not the small size
397 imageBase = QStringLiteral("thumb");
398 break;
399 case MediumImage:
400 imageBase = QStringLiteral("medium");
401 break;
402 case LargeImage:
403 imageBase = QStringLiteral("large");
404 break;
405 case NoImage:
406 m_covers.clear();
407 return; // no need to read anything
408 break;
409 }
410
411 QString baseUrl = coverDataMap_.value(QStringLiteral("base_url")).toMap()
412 .value(imageBase).toString();
413
414 QVariantMap coverMap = coverDataMap_.value(QStringLiteral("data")).toMap();
415 QMapIterator<QString, QVariant> i(coverMap);
416 while(i.hasNext()) {
417 i.next();
418 foreach(QVariant v, i.value().toList()) {
419 QVariantMap map = v.toMap();
420 if(map.value(QStringLiteral("type")) == QLatin1String("boxart") &&
421 map.value(QStringLiteral("side")) == QLatin1String("front")) {
422 m_covers.insert(i.key(), baseUrl + mapValue(map, "filename"));
423 break;
424 }
425 }
426 }
427 }
428
loadCachedData()429 void TheGamesDBFetcher::loadCachedData() {
430 // The lists of genres, publishers, and developers are separate, with TGDB requesting that
431 // the data be cached heavily and only updated when necessary
432 // read the three cached JSON data file for genres, publishers, and developers
433 // the platform info is sent with each request response, so it doesn't necessarily need
434 // to be cache. But if an update request is used, having the cached platform id is helpful
435
436 QFile genreFile(dataFileName(Genre));
437 if(genreFile.open(QIODevice::ReadOnly)) {
438 updateData(Genre, genreFile.readAll());
439 }
440
441 QFile publisherFile(dataFileName(Publisher));
442 if(publisherFile.open(QIODevice::ReadOnly)) {
443 updateData(Publisher, publisherFile.readAll());
444 }
445
446 QFile developerFile(dataFileName(Developer));
447 if(developerFile.open(QIODevice::ReadOnly)) {
448 updateData(Developer, developerFile.readAll());
449 }
450
451 QFile platformFile(dataFileName(Platform));
452 if(platformFile.open(QIODevice::ReadOnly)) {
453 updateData(Platform, platformFile.readAll());
454 }
455 }
456
updateData(TgdbDataType dataType_,const QByteArray & jsonData_)457 void TheGamesDBFetcher::updateData(TgdbDataType dataType_, const QByteArray& jsonData_) {
458 QString dataName;
459 switch(dataType_) {
460 case Genre:
461 dataName = QStringLiteral("genres");
462 break;
463 case Publisher:
464 dataName = QStringLiteral("publishers");
465 break;
466 case Developer:
467 dataName = QStringLiteral("developers");
468 break;
469 case Platform:
470 dataName = QStringLiteral("platforms");
471 break;
472 }
473
474 QHash<int, QString> dataHash;
475 const QVariantMap topMap = QJsonDocument::fromJson(jsonData_).object().toVariantMap();
476 const QVariantMap resultMap = topMap.value(QStringLiteral("data")).toMap()
477 .value(dataName).toMap();
478 for(QMapIterator<QString, QVariant> i(resultMap); i.hasNext(); ) {
479 i.next();
480 const QVariantMap m = i.value().toMap();
481 dataHash.insert(m.value(QStringLiteral("id")).toInt(), mapValue(m, "name"));
482 }
483
484 // transfer read data into the correct local variable
485 switch(dataType_) {
486 case Genre:
487 m_genres = dataHash;
488 break;
489 case Publisher:
490 m_publishers = dataHash;
491 break;
492 case Developer:
493 m_developers = dataHash;
494 break;
495 case Platform:
496 m_platforms = dataHash;
497 break;
498 }
499 }
500
readDataList(TgdbDataType dataType_)501 void TheGamesDBFetcher::readDataList(TgdbDataType dataType_) {
502 QUrl u(QString::fromLatin1(THEGAMESDB_API_URL));
503 u.setPath(QLatin1String("/v") + QLatin1String(THEGAMESDB_API_VERSION));
504 switch(dataType_) {
505 case Genre:
506 u.setPath(u.path() + QLatin1String("/Genres"));
507 break;
508 case Publisher:
509 u.setPath(u.path() + QLatin1String("/Publishers"));
510 break;
511 case Developer:
512 u.setPath(u.path() + QLatin1String("/Developers"));
513 break;
514 case Platform:
515 myDebug() << "not trying to read platforms";
516 // platforms are not read independently, and are only cached
517 return;
518 }
519 QUrlQuery q;
520 q.addQueryItem(QStringLiteral("apikey"), m_apiKey);
521 u.setQuery(q);
522
523 // u = QUrl::fromLocalFile(dataFileName(dataType_)); // for testing
524 // myDebug() << "Reading" << u;
525 const QByteArray data = FileHandler::readDataFile(u, true);
526 writeDataList(dataType_, data);
527 updateData(dataType_, data);
528 }
529
writeDataList(TgdbDataType dataType_,const QByteArray & data_)530 void TheGamesDBFetcher::writeDataList(TgdbDataType dataType_, const QByteArray& data_) {
531 QFile file(dataFileName(dataType_));
532 if(!file.open(QIODevice::WriteOnly) || file.write(data_) == -1) {
533 myDebug() << "unable to write to" << file.fileName() << file.errorString();
534 return;
535 }
536 file.close();
537 }
538
configWidget(QWidget * parent_) const539 Tellico::Fetch::ConfigWidget* TheGamesDBFetcher::configWidget(QWidget* parent_) const {
540 return new TheGamesDBFetcher::ConfigWidget(parent_, this);
541 }
542
defaultName()543 QString TheGamesDBFetcher::defaultName() {
544 return QStringLiteral("TheGamesDB");
545 }
546
defaultIcon()547 QString TheGamesDBFetcher::defaultIcon() {
548 return favIcon("https://thegamesdb.net");
549 }
550
allOptionalFields()551 Tellico::StringHash TheGamesDBFetcher::allOptionalFields() {
552 StringHash hash;
553 hash[QStringLiteral("num-player")] = i18n("Number of Players");
554 return hash;
555 }
556
dataFileName(TgdbDataType dataType_)557 QString TheGamesDBFetcher::dataFileName(TgdbDataType dataType_) {
558 const QString dataDir = Tellico::saveLocation(QStringLiteral("thegamesdb-data/"));
559 QString fileName;
560 switch(dataType_) {
561 case Genre:
562 fileName = dataDir + QLatin1String("genres.json");
563 break;
564 case Publisher:
565 fileName = dataDir + QLatin1String("publishers.json");
566 break;
567 case Developer:
568 fileName = dataDir + QLatin1String("developers.json");
569 break;
570 case Platform:
571 fileName = dataDir + QLatin1String("platforms.json");
572 break;
573 }
574 return fileName;
575 }
576
ConfigWidget(QWidget * parent_,const TheGamesDBFetcher * fetcher_)577 TheGamesDBFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const TheGamesDBFetcher* fetcher_)
578 : Fetch::ConfigWidget(parent_) {
579 QGridLayout* l = new QGridLayout(optionsWidget());
580 l->setSpacing(4);
581 l->setColumnStretch(1, 10);
582
583 int row = -1;
584 QLabel* al = new QLabel(i18n("Registration is required for accessing this data source. "
585 "If you agree to the terms and conditions, <a href='%1'>sign "
586 "up for an account</a>, and enter your information below.",
587 QLatin1String("https://forums.thegamesdb.net/viewforum.php?f=10")),
588 optionsWidget());
589 al->setOpenExternalLinks(true);
590 al->setWordWrap(true);
591 ++row;
592 l->addWidget(al, row, 0, 1, 2);
593 // richtext gets weird with size
594 al->setMinimumWidth(al->sizeHint().width());
595
596 QLabel* label = new QLabel(i18n("Access key: "), optionsWidget());
597 l->addWidget(label, ++row, 0);
598
599 m_apiKeyEdit = new QLineEdit(optionsWidget());
600 connect(m_apiKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified);
601 l->addWidget(m_apiKeyEdit, row, 1);
602 label->setBuddy(m_apiKeyEdit);
603
604 label = new QLabel(i18n("&Image size: "), optionsWidget());
605 l->addWidget(label, ++row, 0);
606 m_imageCombo = new GUI::ComboBox(optionsWidget());
607 m_imageCombo->addItem(i18n("Small Image"), SmallImage);
608 m_imageCombo->addItem(i18n("Medium Image"), MediumImage);
609 m_imageCombo->addItem(i18n("Large Image"), LargeImage);
610 m_imageCombo->addItem(i18n("No Image"), NoImage);
611 void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated;
612 connect(m_imageCombo, activatedInt, this, &ConfigWidget::slotSetModified);
613 l->addWidget(m_imageCombo, row, 1);
614 QString w = i18n("The cover image may be downloaded as well. However, too many large images in the "
615 "collection may degrade performance.");
616 label->setWhatsThis(w);
617 m_imageCombo->setWhatsThis(w);
618 label->setBuddy(m_imageCombo);
619
620 l->setRowStretch(++row, 10);
621
622 if(fetcher_) {
623 m_apiKeyEdit->setText(fetcher_->m_apiKey);
624 m_imageCombo->setCurrentData(fetcher_->m_imageSize);
625 } else { // defaults
626 m_apiKeyEdit->setText(Tellico::reverseObfuscate(THEGAMESDB_MAGIC_TOKEN));
627 m_imageCombo->setCurrentData(SmallImage);
628 }
629
630 // now add additional fields widget
631 addFieldsWidget(TheGamesDBFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList());
632 }
633
saveConfigHook(KConfigGroup & config_)634 void TheGamesDBFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) {
635 const QString apiKey = m_apiKeyEdit->text().trimmed();
636 if(!apiKey.isEmpty() && apiKey != Tellico::reverseObfuscate(THEGAMESDB_MAGIC_TOKEN)) {
637 config_.writeEntry("API Key", apiKey);
638 }
639 const int n = m_imageCombo->currentData().toInt();
640 config_.writeEntry("Image Size", n);
641 }
642
preferredName() const643 QString TheGamesDBFetcher::ConfigWidget::preferredName() const {
644 return TheGamesDBFetcher::defaultName();
645 }
646