Provide torrent creation feature via WebAPI
[qBittorrent.git] / src / gui / search / pluginselectdialog.cpp
blobf549ec410c05ed30c004598a1e6a26661efee913
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>
41 #include "base/global.h"
42 #include "base/net/downloadmanager.h"
43 #include "base/preferences.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) u"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(u"Size"_s))
67 , m_pluginManager(pluginManager)
69 m_ui->setupUi(this);
71 m_ui->pluginsTree->setRootIsDecorated(false);
72 m_ui->pluginsTree->hideColumn(PLUGIN_ID);
73 m_ui->pluginsTree->header()->setFirstSectionMovable(true);
74 m_ui->pluginsTree->header()->setSortIndicator(0, Qt::AscendingOrder);
76 m_ui->actionUninstall->setIcon(UIThemeManager::instance()->getIcon(u"list-remove"_s));
78 connect(m_ui->actionEnable, &QAction::toggled, this, &PluginSelectDialog::enableSelection);
79 connect(m_ui->pluginsTree, &QTreeWidget::customContextMenuRequested, this, &PluginSelectDialog::displayContextMenu);
80 connect(m_ui->pluginsTree, &QTreeWidget::itemDoubleClicked, this, &PluginSelectDialog::togglePluginState);
82 loadSupportedSearchPlugins();
84 connect(m_pluginManager, &SearchPluginManager::pluginInstalled, this, &PluginSelectDialog::pluginInstalled);
85 connect(m_pluginManager, &SearchPluginManager::pluginInstallationFailed, this, &PluginSelectDialog::pluginInstallationFailed);
86 connect(m_pluginManager, &SearchPluginManager::pluginUpdated, this, &PluginSelectDialog::pluginUpdated);
87 connect(m_pluginManager, &SearchPluginManager::pluginUpdateFailed, this, &PluginSelectDialog::pluginUpdateFailed);
88 connect(m_pluginManager, &SearchPluginManager::checkForUpdatesFinished, this, &PluginSelectDialog::checkForUpdatesFinished);
89 connect(m_pluginManager, &SearchPluginManager::checkForUpdatesFailed, this, &PluginSelectDialog::checkForUpdatesFailed);
91 if (const QSize dialogSize = m_storeDialogSize; dialogSize.isValid())
92 resize(dialogSize);
95 PluginSelectDialog::~PluginSelectDialog()
97 m_storeDialogSize = size();
98 delete m_ui;
101 void PluginSelectDialog::dropEvent(QDropEvent *event)
103 event->acceptProposedAction();
105 QStringList files;
106 if (event->mimeData()->hasUrls())
108 for (const QUrl &url : asConst(event->mimeData()->urls()))
110 if (!url.isEmpty())
112 if (url.scheme().compare(u"file", Qt::CaseInsensitive) == 0)
113 files << url.toLocalFile();
114 else
115 files << url.toString();
119 else
121 files = event->mimeData()->text().split(u'\n');
124 if (files.isEmpty()) return;
126 for (const QString &file : asConst(files))
128 qDebug("dropped %s", qUtf8Printable(file));
129 startAsyncOp();
130 m_pluginManager->installPlugin(file);
134 // Decode if we accept drag 'n drop or not
135 void PluginSelectDialog::dragEnterEvent(QDragEnterEvent *event)
137 for (const QString &mime : asConst(event->mimeData()->formats()))
139 qDebug("mimeData: %s", qUtf8Printable(mime));
142 if (event->mimeData()->hasFormat(u"text/plain"_s) || event->mimeData()->hasFormat(u"text/uri-list"_s))
144 event->acceptProposedAction();
148 void PluginSelectDialog::on_updateButton_clicked()
150 startAsyncOp();
151 m_pluginManager->checkForUpdates();
154 void PluginSelectDialog::togglePluginState(QTreeWidgetItem *item, int)
156 PluginInfo *plugin = m_pluginManager->pluginInfo(item->text(PLUGIN_ID));
157 m_pluginManager->enablePlugin(plugin->name, !plugin->enabled);
158 if (plugin->enabled)
160 item->setText(PLUGIN_STATE, tr("Yes"));
161 setRowColor(m_ui->pluginsTree->indexOfTopLevelItem(item), u"green"_s);
163 else
165 item->setText(PLUGIN_STATE, tr("No"));
166 setRowColor(m_ui->pluginsTree->indexOfTopLevelItem(item), u"red"_s);
170 void PluginSelectDialog::displayContextMenu()
172 // Enable/disable pause/start action given the DL state
173 const QList<QTreeWidgetItem *> items = m_ui->pluginsTree->selectedItems();
174 if (items.isEmpty()) return;
176 QMenu *myContextMenu = new QMenu(this);
177 myContextMenu->setAttribute(Qt::WA_DeleteOnClose);
179 const QString firstID = items.first()->text(PLUGIN_ID);
180 m_ui->actionEnable->setChecked(m_pluginManager->pluginInfo(firstID)->enabled);
181 myContextMenu->addAction(m_ui->actionEnable);
182 myContextMenu->addSeparator();
183 myContextMenu->addAction(m_ui->actionUninstall);
185 myContextMenu->popup(QCursor::pos());
188 void PluginSelectDialog::on_closeButton_clicked()
190 close();
193 void PluginSelectDialog::on_actionUninstall_triggered()
195 bool error = false;
196 for (QTreeWidgetItem *item : asConst(m_ui->pluginsTree->selectedItems()))
198 int index = m_ui->pluginsTree->indexOfTopLevelItem(item);
199 Q_ASSERT(index != -1);
200 QString id = item->text(PLUGIN_ID);
201 if (m_pluginManager->uninstallPlugin(id))
203 delete item;
205 else
207 error = true;
208 // Disable it instead
209 m_pluginManager->enablePlugin(id, false);
210 item->setText(PLUGIN_STATE, tr("No"));
211 setRowColor(index, u"red"_s);
215 if (error)
216 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."));
217 else
218 QMessageBox::information(this, tr("Uninstall success"), tr("All selected plugins were uninstalled successfully"));
221 void PluginSelectDialog::enableSelection(bool enable)
223 for (QTreeWidgetItem *item : asConst(m_ui->pluginsTree->selectedItems()))
225 int index = m_ui->pluginsTree->indexOfTopLevelItem(item);
226 Q_ASSERT(index != -1);
227 QString id = item->text(PLUGIN_ID);
228 m_pluginManager->enablePlugin(id, enable);
229 if (enable)
231 item->setText(PLUGIN_STATE, tr("Yes"));
232 setRowColor(index, u"green"_s);
234 else
236 item->setText(PLUGIN_STATE, tr("No"));
237 setRowColor(index, u"red"_s);
242 // Set the color of a row in data model
243 void PluginSelectDialog::setRowColor(const int row, const QString &color)
245 QTreeWidgetItem *item = m_ui->pluginsTree->topLevelItem(row);
246 for (int i = 0; i < m_ui->pluginsTree->columnCount(); ++i)
248 item->setData(i, Qt::ForegroundRole, QColor(color));
252 QVector<QTreeWidgetItem*> PluginSelectDialog::findItemsWithUrl(const QString &url)
254 QVector<QTreeWidgetItem*> res;
255 res.reserve(m_ui->pluginsTree->topLevelItemCount());
257 for (int i = 0; i < m_ui->pluginsTree->topLevelItemCount(); ++i)
259 QTreeWidgetItem *item = m_ui->pluginsTree->topLevelItem(i);
260 if (url.startsWith(item->text(PLUGIN_URL), Qt::CaseInsensitive))
261 res << item;
264 return res;
267 QTreeWidgetItem *PluginSelectDialog::findItemWithID(const QString &id)
269 for (int i = 0; i < m_ui->pluginsTree->topLevelItemCount(); ++i)
271 QTreeWidgetItem *item = m_ui->pluginsTree->topLevelItem(i);
272 if (id == item->text(PLUGIN_ID))
273 return item;
276 return nullptr;
279 void PluginSelectDialog::loadSupportedSearchPlugins()
281 // Some clean up first
282 m_ui->pluginsTree->clear();
283 for (const QString &name : asConst(m_pluginManager->allPlugins()))
284 addNewPlugin(name);
287 void PluginSelectDialog::addNewPlugin(const QString &pluginName)
289 auto *item = new QTreeWidgetItem(m_ui->pluginsTree);
290 PluginInfo *plugin = m_pluginManager->pluginInfo(pluginName);
291 item->setText(PLUGIN_NAME, plugin->fullName);
292 item->setText(PLUGIN_URL, plugin->url);
293 item->setText(PLUGIN_ID, plugin->name);
294 if (plugin->enabled)
296 item->setText(PLUGIN_STATE, tr("Yes"));
297 setRowColor(m_ui->pluginsTree->indexOfTopLevelItem(item), u"green"_s);
299 else
301 item->setText(PLUGIN_STATE, tr("No"));
302 setRowColor(m_ui->pluginsTree->indexOfTopLevelItem(item), u"red"_s);
304 // Handle icon
305 if (plugin->iconPath.exists())
307 // Good, we already have the icon
308 item->setData(PLUGIN_NAME, Qt::DecorationRole, QIcon(plugin->iconPath.data()));
310 else
312 // Icon is missing, we must download it
313 using namespace Net;
314 DownloadManager::instance()->download(
315 DownloadRequest(plugin->url + u"/favicon.ico").saveToFile(true)
316 , Preferences::instance()->useProxyForGeneralPurposes(), this, &PluginSelectDialog::iconDownloadFinished);
318 item->setText(PLUGIN_VERSION, plugin->version.toString());
321 void PluginSelectDialog::startAsyncOp()
323 ++m_asyncOps;
324 if (m_asyncOps == 1)
325 setCursor(QCursor(Qt::WaitCursor));
328 void PluginSelectDialog::finishAsyncOp()
330 --m_asyncOps;
331 if (m_asyncOps == 0)
332 setCursor(QCursor(Qt::ArrowCursor));
335 void PluginSelectDialog::finishPluginUpdate()
337 --m_pendingUpdates;
338 if ((m_pendingUpdates == 0) && !m_updatedPlugins.isEmpty())
340 m_updatedPlugins.sort(Qt::CaseInsensitive);
341 QMessageBox::information(this, tr("Search plugin update"), tr("Plugins installed or updated: %1").arg(m_updatedPlugins.join(u", ")));
342 m_updatedPlugins.clear();
346 void PluginSelectDialog::on_installButton_clicked()
348 auto *dlg = new PluginSourceDialog(this);
349 dlg->setAttribute(Qt::WA_DeleteOnClose);
350 connect(dlg, &PluginSourceDialog::askForLocalFile, this, &PluginSelectDialog::askForLocalPlugin);
351 connect(dlg, &PluginSourceDialog::askForUrl, this, &PluginSelectDialog::askForPluginUrl);
352 dlg->show();
355 void PluginSelectDialog::askForPluginUrl()
357 bool ok = false;
358 QString clipTxt = qApp->clipboard()->text();
359 auto defaultUrl = u"http://"_s;
360 if (Net::DownloadManager::hasSupportedScheme(clipTxt) && clipTxt.endsWith(u".py"))
361 defaultUrl = clipTxt;
362 QString url = AutoExpandableDialog::getText(
363 this, tr("New search engine plugin URL"),
364 tr("URL:"), QLineEdit::Normal, defaultUrl, &ok
367 while (ok && !url.isEmpty() && !url.endsWith(u".py"))
369 QMessageBox::warning(this, tr("Invalid link"), tr("The link doesn't seem to point to a search engine plugin."));
370 url = AutoExpandableDialog::getText(
371 this, tr("New search engine plugin URL"),
372 tr("URL:"), QLineEdit::Normal, url, &ok
376 if (ok && !url.isEmpty())
378 startAsyncOp();
379 m_pluginManager->installPlugin(url);
383 void PluginSelectDialog::askForLocalPlugin()
385 const QStringList pathsList = QFileDialog::getOpenFileNames(
386 nullptr, tr("Select search plugins"), QDir::homePath(),
387 (tr("qBittorrent search plugin") + u" (*.py)"));
388 for (const QString &path : pathsList)
390 startAsyncOp();
391 m_pluginManager->installPlugin(path);
395 void PluginSelectDialog::iconDownloadFinished(const Net::DownloadResult &result)
397 if (result.status != Net::DownloadStatus::Success)
399 qDebug("Could not download favicon: %s, reason: %s", qUtf8Printable(result.url), qUtf8Printable(result.errorString));
400 return;
403 const Path filePath = result.filePath;
405 // Icon downloaded
406 QIcon icon {filePath.data()};
407 // Detect a non-decodable icon
408 QList<QSize> sizes = icon.availableSizes();
409 bool invalid = (sizes.isEmpty() || icon.pixmap(sizes.first()).isNull());
410 if (!invalid)
412 for (QTreeWidgetItem *item : asConst(findItemsWithUrl(result.url)))
414 QString id = item->text(PLUGIN_ID);
415 PluginInfo *plugin = m_pluginManager->pluginInfo(id);
416 if (!plugin) continue;
418 const QString ext = result.url.endsWith(u".ico", Qt::CaseInsensitive) ? u".ico"_s : u".png"_s;
419 const Path iconPath = SearchPluginManager::pluginsLocation() / Path(id + ext);
420 if (Utils::Fs::copyFile(filePath, iconPath))
422 // This 2nd check is necessary. Some favicons (eg from piratebay)
423 // decode fine without an ext, but fail to do so when appending the ext
424 // from the url. Probably a Qt bug.
425 QIcon iconWithExt {iconPath.data()};
426 QList<QSize> sizesExt = iconWithExt.availableSizes();
427 bool invalidExt = (sizesExt.isEmpty() || iconWithExt.pixmap(sizesExt.first()).isNull());
428 if (invalidExt)
430 Utils::Fs::removeFile(iconPath);
431 continue;
434 item->setData(PLUGIN_NAME, Qt::DecorationRole, iconWithExt);
435 m_pluginManager->updateIconPath(plugin);
439 // Delete tmp file
440 Utils::Fs::removeFile(filePath);
443 void PluginSelectDialog::checkForUpdatesFinished(const QHash<QString, PluginVersion> &updateInfo)
445 finishAsyncOp();
446 if (updateInfo.isEmpty())
448 QMessageBox::information(this, tr("Search plugin update"), tr("All your plugins are already up to date."));
449 return;
452 for (auto i = updateInfo.cbegin(); i != updateInfo.cend(); ++i)
454 startAsyncOp();
455 ++m_pendingUpdates;
456 m_pluginManager->updatePlugin(i.key());
460 void PluginSelectDialog::checkForUpdatesFailed(const QString &reason)
462 finishAsyncOp();
463 QMessageBox::warning(this, tr("Search plugin update"), tr("Sorry, couldn't check for plugin updates. %1").arg(reason));
466 void PluginSelectDialog::pluginInstalled(const QString &name)
468 addNewPlugin(name);
469 finishAsyncOp();
470 m_updatedPlugins.append(name);
471 finishPluginUpdate();
474 void PluginSelectDialog::pluginInstallationFailed(const QString &name, const QString &reason)
476 finishAsyncOp();
477 QMessageBox::information(this, tr("Search plugin install")
478 , tr("Couldn't install \"%1\" search engine plugin. %2").arg(name, reason));
479 finishPluginUpdate();
482 void PluginSelectDialog::pluginUpdated(const QString &name)
484 finishAsyncOp();
485 PluginVersion version = m_pluginManager->pluginInfo(name)->version;
486 QTreeWidgetItem *item = findItemWithID(name);
487 item->setText(PLUGIN_VERSION, version.toString());
488 m_updatedPlugins.append(name);
489 finishPluginUpdate();
492 void PluginSelectDialog::pluginUpdateFailed(const QString &name, const QString &reason)
494 finishAsyncOp();
495 QMessageBox::information(this, tr("Search plugin update")
496 , tr("Couldn't update \"%1\" search engine plugin. %2").arg(name, reason));
497 finishPluginUpdate();