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"
34 #include <QFileDialog>
35 #include <QHeaderView>
36 #include <QImageReader>
38 #include <QMessageBox>
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
63 PluginSelectDialog::PluginSelectDialog(SearchPluginManager
*pluginManager
, QWidget
*parent
)
65 , m_ui(new Ui::PluginSelectDialog
)
66 , m_storeDialogSize(SETTINGS_KEY(u
"Size"_s
))
67 , m_pluginManager(pluginManager
)
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())
95 PluginSelectDialog::~PluginSelectDialog()
97 m_storeDialogSize
= size();
101 void PluginSelectDialog::dropEvent(QDropEvent
*event
)
103 event
->acceptProposedAction();
106 if (event
->mimeData()->hasUrls())
108 for (const QUrl
&url
: asConst(event
->mimeData()->urls()))
112 if (url
.scheme().compare(u
"file", Qt::CaseInsensitive
) == 0)
113 files
<< url
.toLocalFile();
115 files
<< url
.toString();
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
));
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()
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
);
160 item
->setText(PLUGIN_STATE
, tr("Yes"));
161 setRowColor(m_ui
->pluginsTree
->indexOfTopLevelItem(item
), u
"green"_s
);
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()
193 void PluginSelectDialog::on_actionUninstall_triggered()
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
))
208 // Disable it instead
209 m_pluginManager
->enablePlugin(id
, false);
210 item
->setText(PLUGIN_STATE
, tr("No"));
211 setRowColor(index
, u
"red"_s
);
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."));
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
);
231 item
->setText(PLUGIN_STATE
, tr("Yes"));
232 setRowColor(index
, u
"green"_s
);
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
))
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
))
279 void PluginSelectDialog::loadSupportedSearchPlugins()
281 // Some clean up first
282 m_ui
->pluginsTree
->clear();
283 for (const QString
&name
: asConst(m_pluginManager
->allPlugins()))
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
);
296 item
->setText(PLUGIN_STATE
, tr("Yes"));
297 setRowColor(m_ui
->pluginsTree
->indexOfTopLevelItem(item
), u
"green"_s
);
301 item
->setText(PLUGIN_STATE
, tr("No"));
302 setRowColor(m_ui
->pluginsTree
->indexOfTopLevelItem(item
), u
"red"_s
);
305 if (plugin
->iconPath
.exists())
307 // Good, we already have the icon
308 item
->setData(PLUGIN_NAME
, Qt::DecorationRole
, QIcon(plugin
->iconPath
.data()));
312 // Icon is missing, we must download it
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()
325 setCursor(QCursor(Qt::WaitCursor
));
328 void PluginSelectDialog::finishAsyncOp()
332 setCursor(QCursor(Qt::ArrowCursor
));
335 void PluginSelectDialog::finishPluginUpdate()
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
);
355 void PluginSelectDialog::askForPluginUrl()
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())
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
)
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
));
403 const Path filePath
= result
.filePath
;
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());
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());
430 Utils::Fs::removeFile(iconPath
);
434 item
->setData(PLUGIN_NAME
, Qt::DecorationRole
, iconWithExt
);
435 m_pluginManager
->updateIconPath(plugin
);
440 Utils::Fs::removeFile(filePath
);
443 void PluginSelectDialog::checkForUpdatesFinished(const QHash
<QString
, PluginVersion
> &updateInfo
)
446 if (updateInfo
.isEmpty())
448 QMessageBox::information(this, tr("Search plugin update"), tr("All your plugins are already up to date."));
452 for (auto i
= updateInfo
.cbegin(); i
!= updateInfo
.cend(); ++i
)
456 m_pluginManager
->updatePlugin(i
.key());
460 void PluginSelectDialog::checkForUpdatesFailed(const QString
&reason
)
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
)
470 m_updatedPlugins
.append(name
);
471 finishPluginUpdate();
474 void PluginSelectDialog::pluginInstallationFailed(const QString
&name
, const QString
&reason
)
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
)
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
)
495 QMessageBox::information(this, tr("Search plugin update")
496 , tr("Couldn't update \"%1\" search engine plugin. %2").arg(name
, reason
));
497 finishPluginUpdate();