Sync translations from Transifex and run lupdate
[qBittorrent.git] / src / gui / search / pluginselectdialog.cpp
blob9ab36b187ef3c51a72ff1b1acd90b87075e1a0ba
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru>
4 * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * In addition, as a special exception, the copyright holders give permission to
21 * link this program with the OpenSSL project's "OpenSSL" library (or with
22 * modified versions of it that use the same license as the "OpenSSL" library),
23 * and distribute the linked executables. You must obey the GNU General Public
24 * License in all respects for all of the code used other than "OpenSSL". If you
25 * modify file(s), you may extend this exception to your version of the file(s),
26 * but you are not obligated to do so. If you do not wish to do so, delete this
27 * exception statement from your version.
30 #include "pluginselectdialog.h"
32 #include <QClipboard>
33 #include <QDropEvent>
34 #include <QFileDialog>
35 #include <QHeaderView>
36 #include <QImageReader>
37 #include <QMenu>
38 #include <QMessageBox>
39 #include <QMimeData>
40 #include <QTableView>
42 #include "base/global.h"
43 #include "base/net/downloadmanager.h"
44 #include "base/utils/fs.h"
45 #include "gui/autoexpandabledialog.h"
46 #include "gui/uithememanager.h"
47 #include "gui/utils.h"
48 #include "pluginsourcedialog.h"
49 #include "searchwidget.h"
50 #include "ui_pluginselectdialog.h"
52 #define SETTINGS_KEY(name) "SearchPluginSelectDialog/" name
54 enum PluginColumns
56 PLUGIN_NAME,
57 PLUGIN_VERSION,
58 PLUGIN_URL,
59 PLUGIN_STATE,
60 PLUGIN_ID
63 PluginSelectDialog::PluginSelectDialog(SearchPluginManager *pluginManager, QWidget *parent)
64 : QDialog(parent)
65 , m_ui(new Ui::PluginSelectDialog)
66 , m_storeDialogSize(SETTINGS_KEY("Size"))
67 , m_pluginManager(pluginManager)
69 m_ui->setupUi(this);
70 setAttribute(Qt::WA_DeleteOnClose);
72 // This hack fixes reordering of first column with Qt5.
73 // https://github.com/qtproject/qtbase/commit/e0fc088c0c8bc61dbcaf5928b24986cd61a22777
74 QTableView unused;
75 unused.setVerticalHeader(m_ui->pluginsTree->header());
76 m_ui->pluginsTree->header()->setParent(m_ui->pluginsTree);
77 unused.setVerticalHeader(new QHeaderView(Qt::Horizontal));
79 m_ui->pluginsTree->setRootIsDecorated(false);
80 m_ui->pluginsTree->hideColumn(PLUGIN_ID);
81 m_ui->pluginsTree->header()->setSortIndicator(0, Qt::AscendingOrder);
83 m_ui->actionUninstall->setIcon(UIThemeManager::instance()->getIcon("list-remove"));
85 connect(m_ui->actionEnable, &QAction::toggled, this, &PluginSelectDialog::enableSelection);
86 connect(m_ui->pluginsTree, &QTreeWidget::customContextMenuRequested, this, &PluginSelectDialog::displayContextMenu);
87 connect(m_ui->pluginsTree, &QTreeWidget::itemDoubleClicked, this, &PluginSelectDialog::togglePluginState);
89 loadSupportedSearchPlugins();
91 connect(m_pluginManager, &SearchPluginManager::pluginInstalled, this, &PluginSelectDialog::pluginInstalled);
92 connect(m_pluginManager, &SearchPluginManager::pluginInstallationFailed, this, &PluginSelectDialog::pluginInstallationFailed);
93 connect(m_pluginManager, &SearchPluginManager::pluginUpdated, this, &PluginSelectDialog::pluginUpdated);
94 connect(m_pluginManager, &SearchPluginManager::pluginUpdateFailed, this, &PluginSelectDialog::pluginUpdateFailed);
95 connect(m_pluginManager, &SearchPluginManager::checkForUpdatesFinished, this, &PluginSelectDialog::checkForUpdatesFinished);
96 connect(m_pluginManager, &SearchPluginManager::checkForUpdatesFailed, this, &PluginSelectDialog::checkForUpdatesFailed);
98 Utils::Gui::resize(this, m_storeDialogSize);
99 show();
102 PluginSelectDialog::~PluginSelectDialog()
104 m_storeDialogSize = size();
105 delete m_ui;
108 void PluginSelectDialog::dropEvent(QDropEvent *event)
110 event->acceptProposedAction();
112 QStringList files;
113 if (event->mimeData()->hasUrls())
115 for (const QUrl &url : asConst(event->mimeData()->urls()))
117 if (!url.isEmpty())
119 if (url.scheme().compare("file", Qt::CaseInsensitive) == 0)
120 files << url.toLocalFile();
121 else
122 files << url.toString();
126 else
128 files = event->mimeData()->text().split('\n');
131 if (files.isEmpty()) return;
133 for (const QString &file : asConst(files))
135 qDebug("dropped %s", qUtf8Printable(file));
136 startAsyncOp();
137 m_pluginManager->installPlugin(file);
141 // Decode if we accept drag 'n drop or not
142 void PluginSelectDialog::dragEnterEvent(QDragEnterEvent *event)
144 for (const QString &mime : asConst(event->mimeData()->formats()))
146 qDebug("mimeData: %s", qUtf8Printable(mime));
149 if (event->mimeData()->hasFormat(QLatin1String("text/plain")) || event->mimeData()->hasFormat(QLatin1String("text/uri-list")))
151 event->acceptProposedAction();
155 void PluginSelectDialog::on_updateButton_clicked()
157 startAsyncOp();
158 m_pluginManager->checkForUpdates();
161 void PluginSelectDialog::togglePluginState(QTreeWidgetItem *item, int)
163 PluginInfo *plugin = m_pluginManager->pluginInfo(item->text(PLUGIN_ID));
164 m_pluginManager->enablePlugin(plugin->name, !plugin->enabled);
165 if (plugin->enabled)
167 item->setText(PLUGIN_STATE, tr("Yes"));
168 setRowColor(m_ui->pluginsTree->indexOfTopLevelItem(item), "green");
170 else
172 item->setText(PLUGIN_STATE, tr("No"));
173 setRowColor(m_ui->pluginsTree->indexOfTopLevelItem(item), "red");
177 void PluginSelectDialog::displayContextMenu(const QPoint &)
179 // Enable/disable pause/start action given the DL state
180 const QList<QTreeWidgetItem *> items = m_ui->pluginsTree->selectedItems();
181 if (items.isEmpty()) return;
183 QMenu *myContextMenu = new QMenu(this);
184 myContextMenu->setAttribute(Qt::WA_DeleteOnClose);
186 const QString firstID = items.first()->text(PLUGIN_ID);
187 m_ui->actionEnable->setChecked(m_pluginManager->pluginInfo(firstID)->enabled);
188 myContextMenu->addAction(m_ui->actionEnable);
189 myContextMenu->addSeparator();
190 myContextMenu->addAction(m_ui->actionUninstall);
192 myContextMenu->popup(QCursor::pos());
195 void PluginSelectDialog::on_closeButton_clicked()
197 close();
200 void PluginSelectDialog::on_actionUninstall_triggered()
202 bool error = false;
203 for (QTreeWidgetItem *item : asConst(m_ui->pluginsTree->selectedItems()))
205 int index = m_ui->pluginsTree->indexOfTopLevelItem(item);
206 Q_ASSERT(index != -1);
207 QString id = item->text(PLUGIN_ID);
208 if (m_pluginManager->uninstallPlugin(id))
210 delete item;
212 else
214 error = true;
215 // Disable it instead
216 m_pluginManager->enablePlugin(id, false);
217 item->setText(PLUGIN_STATE, tr("No"));
218 setRowColor(index, "red");
222 if (error)
223 QMessageBox::warning(this, tr("Uninstall warning"), tr("Some plugins could not be uninstalled because they are included in qBittorrent. Only the ones you added yourself can be uninstalled.\nThose plugins were disabled."));
224 else
225 QMessageBox::information(this, tr("Uninstall success"), tr("All selected plugins were uninstalled successfully"));
228 void PluginSelectDialog::enableSelection(bool enable)
230 for (QTreeWidgetItem *item : asConst(m_ui->pluginsTree->selectedItems()))
232 int index = m_ui->pluginsTree->indexOfTopLevelItem(item);
233 Q_ASSERT(index != -1);
234 QString id = item->text(PLUGIN_ID);
235 m_pluginManager->enablePlugin(id, enable);
236 if (enable)
238 item->setText(PLUGIN_STATE, tr("Yes"));
239 setRowColor(index, "green");
241 else
243 item->setText(PLUGIN_STATE, tr("No"));
244 setRowColor(index, "red");
249 // Set the color of a row in data model
250 void PluginSelectDialog::setRowColor(const int row, const QString &color)
252 QTreeWidgetItem *item = m_ui->pluginsTree->topLevelItem(row);
253 for (int i = 0; i < m_ui->pluginsTree->columnCount(); ++i)
255 item->setData(i, Qt::ForegroundRole, QColor(color));
259 QVector<QTreeWidgetItem*> PluginSelectDialog::findItemsWithUrl(const QString &url)
261 QVector<QTreeWidgetItem*> res;
262 res.reserve(m_ui->pluginsTree->topLevelItemCount());
264 for (int i = 0; i < m_ui->pluginsTree->topLevelItemCount(); ++i)
266 QTreeWidgetItem *item = m_ui->pluginsTree->topLevelItem(i);
267 if (url.startsWith(item->text(PLUGIN_URL), Qt::CaseInsensitive))
268 res << item;
271 return res;
274 QTreeWidgetItem *PluginSelectDialog::findItemWithID(const QString &id)
276 for (int i = 0; i < m_ui->pluginsTree->topLevelItemCount(); ++i)
278 QTreeWidgetItem *item = m_ui->pluginsTree->topLevelItem(i);
279 if (id == item->text(PLUGIN_ID))
280 return item;
283 return nullptr;
286 void PluginSelectDialog::loadSupportedSearchPlugins()
288 // Some clean up first
289 m_ui->pluginsTree->clear();
290 for (const QString &name : asConst(m_pluginManager->allPlugins()))
291 addNewPlugin(name);
294 void PluginSelectDialog::addNewPlugin(const QString &pluginName)
296 auto *item = new QTreeWidgetItem(m_ui->pluginsTree);
297 PluginInfo *plugin = m_pluginManager->pluginInfo(pluginName);
298 item->setText(PLUGIN_NAME, plugin->fullName);
299 item->setText(PLUGIN_URL, plugin->url);
300 item->setText(PLUGIN_ID, plugin->name);
301 if (plugin->enabled)
303 item->setText(PLUGIN_STATE, tr("Yes"));
304 setRowColor(m_ui->pluginsTree->indexOfTopLevelItem(item), "green");
306 else
308 item->setText(PLUGIN_STATE, tr("No"));
309 setRowColor(m_ui->pluginsTree->indexOfTopLevelItem(item), "red");
311 // Handle icon
312 if (QFile::exists(plugin->iconPath))
314 // Good, we already have the icon
315 item->setData(PLUGIN_NAME, Qt::DecorationRole, QIcon(plugin->iconPath));
317 else
319 // Icon is missing, we must download it
320 using namespace Net;
321 DownloadManager::instance()->download(
322 DownloadRequest(plugin->url + "/favicon.ico").saveToFile(true)
323 , this, &PluginSelectDialog::iconDownloadFinished);
325 item->setText(PLUGIN_VERSION, plugin->version);
328 void PluginSelectDialog::startAsyncOp()
330 ++m_asyncOps;
331 if (m_asyncOps == 1)
332 setCursor(QCursor(Qt::WaitCursor));
335 void PluginSelectDialog::finishAsyncOp()
337 --m_asyncOps;
338 if (m_asyncOps == 0)
339 setCursor(QCursor(Qt::ArrowCursor));
342 void PluginSelectDialog::finishPluginUpdate()
344 --m_pendingUpdates;
345 if ((m_pendingUpdates == 0) && !m_updatedPlugins.isEmpty())
347 m_updatedPlugins.sort(Qt::CaseInsensitive);
348 QMessageBox::information(this, tr("Search plugin update"), tr("Plugins installed or updated: %1").arg(m_updatedPlugins.join(", ")));
349 m_updatedPlugins.clear();
353 void PluginSelectDialog::on_installButton_clicked()
355 auto *dlg = new PluginSourceDialog(this);
356 connect(dlg, &PluginSourceDialog::askForLocalFile, this, &PluginSelectDialog::askForLocalPlugin);
357 connect(dlg, &PluginSourceDialog::askForUrl, this, &PluginSelectDialog::askForPluginUrl);
360 void PluginSelectDialog::askForPluginUrl()
362 bool ok = false;
363 QString clipTxt = qApp->clipboard()->text();
364 QString defaultUrl = "http://";
365 if (Net::DownloadManager::hasSupportedScheme(clipTxt) && clipTxt.endsWith(".py"))
366 defaultUrl = clipTxt;
367 QString url = AutoExpandableDialog::getText(
368 this, tr("New search engine plugin URL"),
369 tr("URL:"), QLineEdit::Normal, defaultUrl, &ok
372 while (ok && !url.isEmpty() && !url.endsWith(".py"))
374 QMessageBox::warning(this, tr("Invalid link"), tr("The link doesn't seem to point to a search engine plugin."));
375 url = AutoExpandableDialog::getText(
376 this, tr("New search engine plugin URL"),
377 tr("URL:"), QLineEdit::Normal, url, &ok
381 if (ok && !url.isEmpty())
383 startAsyncOp();
384 m_pluginManager->installPlugin(url);
388 void PluginSelectDialog::askForLocalPlugin()
390 const QStringList pathsList = QFileDialog::getOpenFileNames(
391 nullptr, tr("Select search plugins"), QDir::homePath(),
392 tr("qBittorrent search plugin") + QLatin1String(" (*.py)")
394 for (const QString &path : pathsList)
396 startAsyncOp();
397 m_pluginManager->installPlugin(path);
401 void PluginSelectDialog::iconDownloadFinished(const Net::DownloadResult &result)
403 if (result.status != Net::DownloadStatus::Success)
405 qDebug("Could not download favicon: %s, reason: %s", qUtf8Printable(result.url), qUtf8Printable(result.errorString));
406 return;
409 const QString filePath = Utils::Fs::toUniformPath(result.filePath);
411 // Icon downloaded
412 QIcon icon(filePath);
413 // Detect a non-decodable icon
414 QList<QSize> sizes = icon.availableSizes();
415 bool invalid = (sizes.isEmpty() || icon.pixmap(sizes.first()).isNull());
416 if (!invalid)
418 for (QTreeWidgetItem *item : asConst(findItemsWithUrl(result.url)))
420 QString id = item->text(PLUGIN_ID);
421 PluginInfo *plugin = m_pluginManager->pluginInfo(id);
422 if (!plugin) continue;
424 QString iconPath = QString("%1/%2.%3")
425 .arg(SearchPluginManager::pluginsLocation()
426 , id
427 , result.url.endsWith(".ico", Qt::CaseInsensitive) ? "ico" : "png");
428 if (QFile::copy(filePath, iconPath))
430 // This 2nd check is necessary. Some favicons (eg from piratebay)
431 // decode fine without an ext, but fail to do so when appending the ext
432 // from the url. Probably a Qt bug.
433 QIcon iconWithExt(iconPath);
434 QList<QSize> sizesExt = iconWithExt.availableSizes();
435 bool invalidExt = (sizesExt.isEmpty() || iconWithExt.pixmap(sizesExt.first()).isNull());
436 if (invalidExt)
438 Utils::Fs::forceRemove(iconPath);
439 continue;
442 item->setData(PLUGIN_NAME, Qt::DecorationRole, iconWithExt);
443 m_pluginManager->updateIconPath(plugin);
447 // Delete tmp file
448 Utils::Fs::forceRemove(filePath);
451 void PluginSelectDialog::checkForUpdatesFinished(const QHash<QString, PluginVersion> &updateInfo)
453 finishAsyncOp();
454 if (updateInfo.isEmpty())
456 QMessageBox::information(this, tr("Search plugin update"), tr("All your plugins are already up to date."));
457 return;
460 for (auto i = updateInfo.cbegin(); i != updateInfo.cend(); ++i)
462 startAsyncOp();
463 ++m_pendingUpdates;
464 m_pluginManager->updatePlugin(i.key());
468 void PluginSelectDialog::checkForUpdatesFailed(const QString &reason)
470 finishAsyncOp();
471 QMessageBox::warning(this, tr("Search plugin update"), tr("Sorry, couldn't check for plugin updates. %1").arg(reason));
474 void PluginSelectDialog::pluginInstalled(const QString &name)
476 addNewPlugin(name);
477 finishAsyncOp();
478 m_updatedPlugins.append(name);
479 finishPluginUpdate();
482 void PluginSelectDialog::pluginInstallationFailed(const QString &name, const QString &reason)
484 finishAsyncOp();
485 QMessageBox::information(this, tr("Search plugin install")
486 , tr("Couldn't install \"%1\" search engine plugin. %2").arg(name, reason));
487 finishPluginUpdate();
490 void PluginSelectDialog::pluginUpdated(const QString &name)
492 finishAsyncOp();
493 PluginVersion version = m_pluginManager->pluginInfo(name)->version;
494 QTreeWidgetItem *item = findItemWithID(name);
495 item->setText(PLUGIN_VERSION, version);
496 m_updatedPlugins.append(name);
497 finishPluginUpdate();
500 void PluginSelectDialog::pluginUpdateFailed(const QString &name, const QString &reason)
502 finishAsyncOp();
503 QMessageBox::information(this, tr("Search plugin update")
504 , tr("Couldn't update \"%1\" search engine plugin. %2").arg(name, reason));
505 finishPluginUpdate();