qBittorrent
addnewtorrentdialog.cpp
Go to the documentation of this file.
1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  *
19  * In addition, as a special exception, the copyright holders give permission to
20  * link this program with the OpenSSL project's "OpenSSL" library (or with
21  * modified versions of it that use the same license as the "OpenSSL" library),
22  * and distribute the linked executables. You must obey the GNU General Public
23  * License in all respects for all of the code used other than "OpenSSL". If you
24  * modify file(s), you may extend this exception to your version of the file(s),
25  * but you are not obligated to do so. If you do not wish to do so, delete this
26  * exception statement from your version.
27  */
28 
29 #include "addnewtorrentdialog.h"
30 
31 #include <algorithm>
32 
33 #include <QDebug>
34 #include <QDir>
35 #include <QFileDialog>
36 #include <QMenu>
37 #include <QPushButton>
38 #include <QShortcut>
39 #include <QString>
40 #include <QUrl>
41 #include <QVector>
42 
49 #include "base/global.h"
51 #include "base/settingsstorage.h"
52 #include "base/torrentfileguard.h"
53 #include "base/utils/compare.h"
54 #include "base/utils/fs.h"
55 #include "base/utils/misc.h"
56 #include "autoexpandabledialog.h"
58 #include "raisedmessagebox.h"
60 #include "torrentcontentmodel.h"
61 #include "ui_addnewtorrentdialog.h"
62 #include "uithememanager.h"
63 #include "utils.h"
64 
65 namespace
66 {
67 #define SETTINGS_KEY(name) "AddNewTorrentDialog/" name
68  const QString KEY_ENABLED = QStringLiteral(SETTINGS_KEY("Enabled"));
69  const QString KEY_TOPLEVEL = QStringLiteral(SETTINGS_KEY("TopLevel"));
70  const QString KEY_SAVEPATHHISTORY = QStringLiteral(SETTINGS_KEY("SavePathHistory"));
71  const QString KEY_DOWNLOADPATHHISTORY = QStringLiteral(SETTINGS_KEY("DownloadPathHistory"));
72  const QString KEY_SAVEPATHHISTORYLENGTH = QStringLiteral(SETTINGS_KEY("SavePathHistoryLength"));
73 
74  // just a shortcut
76  {
78  }
79 
81  {
82  public:
83  FileStorageAdaptor(const BitTorrent::TorrentInfo &torrentInfo, QStringList &filePaths)
84  : m_torrentInfo {torrentInfo}
85  , m_filePaths {filePaths}
86  {
87  Q_ASSERT(filePaths.isEmpty() || (filePaths.size() == torrentInfo.filesCount()));
88  }
89 
90  int filesCount() const override
91  {
92  return m_torrentInfo.filesCount();
93  }
94 
95  qlonglong fileSize(const int index) const override
96  {
97  Q_ASSERT((index >= 0) && (index < filesCount()));
98  return m_torrentInfo.fileSize(index);
99  }
100 
101  QString filePath(const int index) const override
102  {
103  Q_ASSERT((index >= 0) && (index < filesCount()));
104  return (m_filePaths.isEmpty() ? m_torrentInfo.filePath(index) : m_filePaths.at(index));
105  }
106 
107  void renameFile(const int index, const QString &newFilePath) override
108  {
109  Q_ASSERT((index >= 0) && (index < filesCount()));
110  const QString currentFilePath = filePath(index);
111  if (currentFilePath == newFilePath)
112  return;
113 
114  if (m_filePaths.isEmpty())
115  m_filePaths = m_torrentInfo.filePaths();
116 
117  m_filePaths[index] = newFilePath;
118  }
119 
120  private:
122  QStringList &m_filePaths;
123  };
124 
125  // savePath is a folder, not an absolute file path
126  int indexOfPath(const FileSystemPathComboEdit *fsPathEdit, const QString &savePath)
127  {
128  const QDir saveDir {savePath};
129  for (int i = 0; i < fsPathEdit->count(); ++i)
130  {
131  if (QDir(fsPathEdit->item(i)) == saveDir)
132  return i;
133  }
134  return -1;
135  }
136 
137  void setPath(FileSystemPathComboEdit *fsPathEdit, const QString &newPath)
138  {
139  int existingIndex = indexOfPath(fsPathEdit, newPath);
140  if (existingIndex < 0)
141  {
142  // New path, prepend to combo box
143  fsPathEdit->insertItem(0, newPath);
144  existingIndex = 0;
145  }
146 
147  fsPathEdit->setCurrentIndex(existingIndex);
148  }
149 
150  void updatePathHistory(const QString &settingsKey, const QString &path, const int maxLength)
151  {
152  // Add last used save path to the front of history
153 
154  auto pathList = settings()->loadValue<QStringList>(settingsKey);
155  const int selectedSavePathIndex = pathList.indexOf(path);
156  if (selectedSavePathIndex > -1)
157  pathList.move(selectedSavePathIndex, 0);
158  else
159  pathList.prepend(path);
160  settings()->storeValue(settingsKey, QStringList(pathList.mid(0, maxLength)));
161  }
162 }
163 
166 
168  : QDialog(parent)
169  , m_ui(new Ui::AddNewTorrentDialog)
170  , m_torrentParams(inParams)
171  , m_storeDialogSize(SETTINGS_KEY("DialogSize"))
172  , m_storeDefaultCategory(SETTINGS_KEY("DefaultCategory"))
173  , m_storeRememberLastSavePath(SETTINGS_KEY("RememberLastSavePath"))
174 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
175  , m_storeTreeHeaderState("GUI/Qt6/" SETTINGS_KEY("TreeHeaderState"))
176  , m_storeSplitterState("GUI/Qt6/" SETTINGS_KEY("SplitterState"))
177 #else
178  , m_storeTreeHeaderState(SETTINGS_KEY("TreeHeaderState"))
179  , m_storeSplitterState(SETTINGS_KEY("SplitterState"))
180 #endif
181 {
182  // TODO: set dialog file properties using m_torrentParams.filePriorities
183  m_ui->setupUi(this);
184  setAttribute(Qt::WA_DeleteOnClose);
185 
186  m_ui->lblMetaLoading->setVisible(false);
187  m_ui->progMetaLoading->setVisible(false);
188  m_ui->buttonSave->setVisible(false);
189  connect(m_ui->buttonSave, &QPushButton::clicked, this, &AddNewTorrentDialog::saveTorrentFile);
190 
191  m_ui->savePath->setMode(FileSystemPathEdit::Mode::DirectorySave);
192  m_ui->savePath->setDialogCaption(tr("Choose save path"));
193  m_ui->savePath->setMaxVisibleItems(20);
194 
195  const auto *session = BitTorrent::Session::instance();
196 
197  m_ui->downloadPath->setMode(FileSystemPathEdit::Mode::DirectorySave);
198  m_ui->downloadPath->setDialogCaption(tr("Choose save path"));
199  m_ui->downloadPath->setMaxVisibleItems(20);
200 
201  m_ui->startTorrentCheckBox->setChecked(!m_torrentParams.addPaused.value_or(session->isAddTorrentPaused()));
202 
203  m_ui->comboTTM->blockSignals(true); // the TreeView size isn't correct if the slot does it job at this point
204  m_ui->comboTTM->setCurrentIndex(session->isAutoTMMDisabledByDefault() ? 0 : 1);
205  m_ui->comboTTM->blockSignals(false);
206 
209  connect(m_ui->groupBoxDownloadPath, &QGroupBox::toggled, this, &AddNewTorrentDialog::onUseDownloadPathChanged);
210 
211  m_ui->checkBoxRememberLastSavePath->setChecked(m_storeRememberLastSavePath);
212 
213  m_ui->contentLayoutComboBox->setCurrentIndex(
214  static_cast<int>(m_torrentParams.contentLayout.value_or(session->torrentContentLayout())));
215  connect(m_ui->contentLayoutComboBox, &QComboBox::currentIndexChanged, this, &AddNewTorrentDialog::contentLayoutChanged);
216 
217  m_ui->sequentialCheckBox->setChecked(m_torrentParams.sequential);
218  m_ui->firstLastCheckBox->setChecked(m_torrentParams.firstLastPiecePriority);
219 
220  m_ui->skipCheckingCheckBox->setChecked(m_torrentParams.skipChecking);
221  m_ui->doNotDeleteTorrentCheckBox->setVisible(TorrentFileGuard::autoDeleteMode() != TorrentFileGuard::Never);
222 
223  // Load categories
224  QStringList categories = session->categories();
225  std::sort(categories.begin(), categories.end(), Utils::Compare::NaturalLessThan<Qt::CaseInsensitive>());
226  const QString defaultCategory = m_storeDefaultCategory;
227 
228  if (!m_torrentParams.category.isEmpty())
229  m_ui->categoryComboBox->addItem(m_torrentParams.category);
230  if (!defaultCategory.isEmpty())
231  m_ui->categoryComboBox->addItem(defaultCategory);
232  m_ui->categoryComboBox->addItem("");
233 
234  for (const QString &category : asConst(categories))
235  if (category != defaultCategory && category != m_torrentParams.category)
236  m_ui->categoryComboBox->addItem(category);
237 
238  m_ui->contentTreeView->header()->setSortIndicator(0, Qt::AscendingOrder);
239  loadState();
240  // Signal / slots
241  connect(m_ui->doNotDeleteTorrentCheckBox, &QCheckBox::clicked, this, &AddNewTorrentDialog::doNotDeleteTorrentClicked);
242  QShortcut *editHotkey = new QShortcut(Qt::Key_F2, m_ui->contentTreeView, nullptr, nullptr, Qt::WidgetShortcut);
243  connect(editHotkey, &QShortcut::activated, this, &AddNewTorrentDialog::renameSelectedFile);
244  connect(m_ui->contentTreeView, &QAbstractItemView::doubleClicked, this, &AddNewTorrentDialog::renameSelectedFile);
245 
246  m_ui->buttonBox->button(QDialogButtonBox::Ok)->setFocus();
247 }
248 
250 {
251  saveState();
252 
253  delete m_contentDelegate;
254  delete m_ui;
255 }
256 
258 {
259  return settings()->loadValue(KEY_ENABLED, true);
260 }
261 
263 {
265 }
266 
268 {
269  return settings()->loadValue(KEY_TOPLEVEL, true);
270 }
271 
273 {
275 }
276 
278 {
279  const int defaultHistoryLength = 8;
280  const int value = settings()->loadValue(KEY_SAVEPATHHISTORYLENGTH, defaultHistoryLength);
281  return std::clamp(value, minPathHistoryLength, maxPathHistoryLength);
282 }
283 
285 {
286  const int clampedValue = qBound(minPathHistoryLength, value, maxPathHistoryLength);
287  const int oldValue = savePathHistoryLength();
288  if (clampedValue == oldValue)
289  return;
290 
293  , QStringList(settings()->loadValue<QStringList>(KEY_SAVEPATHHISTORY).mid(0, clampedValue)));
294 }
295 
297 {
299  m_ui->splitter->restoreState(m_storeSplitterState);;
300 }
301 
303 {
304  m_storeDialogSize = size();
305  m_storeSplitterState = m_ui->splitter->saveState();
306  if (m_contentModel)
307  m_storeTreeHeaderState = m_ui->contentTreeView->header()->saveState();
308 }
309 
310 void AddNewTorrentDialog::show(const QString &source, const BitTorrent::AddTorrentParams &inParams, QWidget *parent)
311 {
312  auto *dlg = new AddNewTorrentDialog(inParams, parent);
313 
315  {
316  // Launch downloader
320  return;
321  }
322 
323  const BitTorrent::MagnetUri magnetUri(source);
324  const bool isLoaded = magnetUri.isValid()
325  ? dlg->loadMagnet(magnetUri)
326  : dlg->loadTorrentFile(source);
327 
328  if (isLoaded)
329  dlg->QDialog::show();
330  else
331  delete dlg;
332 }
333 
334 void AddNewTorrentDialog::show(const QString &source, QWidget *parent)
335 {
337 }
338 
339 bool AddNewTorrentDialog::loadTorrentFile(const QString &torrentPath)
340 {
341  const QString decodedPath = torrentPath.startsWith("file://", Qt::CaseInsensitive)
342  ? QUrl::fromEncoded(torrentPath.toLocal8Bit()).toLocalFile()
343  : torrentPath;
344 
345  const nonstd::expected<BitTorrent::TorrentInfo, QString> result = BitTorrent::TorrentInfo::loadFromFile(decodedPath);
346  if (!result)
347  {
348  RaisedMessageBox::critical(this, tr("Invalid torrent")
349  , tr("Failed to load the torrent: %1.\nError: %2", "Don't remove the '\n' characters. They insert a newline.")
350  .arg(Utils::Fs::toNativePath(decodedPath), result.error()));
351  return false;
352  }
353 
354  m_torrentInfo = result.value();
355  m_torrentGuard = std::make_unique<TorrentFileGuard>(decodedPath);
356 
357  return loadTorrentImpl();
358 }
359 
361 {
363 
364  // Prevent showing the dialog if download is already present
365  if (BitTorrent::Session::instance()->isKnownTorrent(torrentID))
366  {
367  BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(torrentID);
368  if (torrent)
369  {
370  if (torrent->isPrivate() || m_torrentInfo.isPrivate())
371  {
372  RaisedMessageBox::warning(this, tr("Torrent is already present"), tr("Torrent '%1' is already in the transfer list. Trackers haven't been merged because it is a private torrent.").arg(torrent->name()), QMessageBox::Ok);
373  }
374  else
375  {
376  torrent->addTrackers(m_torrentInfo.trackers());
377  torrent->addUrlSeeds(m_torrentInfo.urlSeeds());
378  RaisedMessageBox::information(this, tr("Torrent is already present"), tr("Torrent '%1' is already in the transfer list. Trackers have been merged.").arg(torrent->name()), QMessageBox::Ok);
379  }
380  }
381  else
382  {
383  RaisedMessageBox::information(this, tr("Torrent is already present"), tr("Torrent is already queued for processing."), QMessageBox::Ok);
384  }
385  return false;
386  }
387 
388  m_ui->labelInfohash1Data->setText(m_torrentInfo.infoHash().v1().isValid() ? m_torrentInfo.infoHash().v1().toString() : tr("N/A"));
389  m_ui->labelInfohash2Data->setText(m_torrentInfo.infoHash().v2().isValid() ? m_torrentInfo.infoHash().v2().toString() : tr("N/A"));
390  setupTreeview();
391  TMMChanged(m_ui->comboTTM->currentIndex());
392 
393  return true;
394 }
395 
397 {
398  if (!magnetUri.isValid())
399  {
400  RaisedMessageBox::critical(this, tr("Invalid magnet link"), tr("This magnet link was not recognized"));
401  return false;
402  }
403 
404  m_torrentGuard = std::make_unique<TorrentFileGuard>();
405 
406  const auto torrentID = BitTorrent::TorrentID::fromInfoHash(magnetUri.infoHash());
407  // Prevent showing the dialog if download is already present
408  if (BitTorrent::Session::instance()->isKnownTorrent(torrentID))
409  {
410  BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(torrentID);
411  if (torrent)
412  {
413  if (torrent->isPrivate())
414  {
415  RaisedMessageBox::warning(this, tr("Torrent is already present"), tr("Torrent '%1' is already in the transfer list. Trackers haven't been merged because it is a private torrent.").arg(torrent->name()), QMessageBox::Ok);
416  }
417  else
418  {
419  torrent->addTrackers(magnetUri.trackers());
420  torrent->addUrlSeeds(magnetUri.urlSeeds());
421  RaisedMessageBox::information(this, tr("Torrent is already present"), tr("Magnet link '%1' is already in the transfer list. Trackers have been merged.").arg(torrent->name()), QMessageBox::Ok);
422  }
423  }
424  else
425  {
426  RaisedMessageBox::information(this, tr("Torrent is already present"), tr("Magnet link is already queued for processing."), QMessageBox::Ok);
427  }
428  return false;
429  }
430 
432 
433  // Set dialog title
434  const QString torrentName = magnetUri.name();
435  setWindowTitle(torrentName.isEmpty() ? tr("Magnet link") : torrentName);
436 
437  setupTreeview();
438  TMMChanged(m_ui->comboTTM->currentIndex());
439 
441  setMetadataProgressIndicator(true, tr("Retrieving metadata..."));
442  m_ui->labelInfohash1Data->setText(magnetUri.infoHash().v1().isValid() ? magnetUri.infoHash().v1().toString() : tr("N/A"));
443  m_ui->labelInfohash2Data->setText(magnetUri.infoHash().v2().isValid() ? magnetUri.infoHash().v2().toString() : tr("N/A"));
444 
445  m_magnetURI = magnetUri;
446  return true;
447 }
448 
449 void AddNewTorrentDialog::showEvent(QShowEvent *event)
450 {
451  QDialog::showEvent(event);
452  if (!isTopLevel()) return;
453 
454  activateWindow();
455  raise();
456 }
457 
459 {
460  // Determine torrent size
461  qlonglong torrentSize = 0;
462 
463  if (hasMetadata())
464  {
465  if (m_contentModel)
466  {
467  const QVector<BitTorrent::DownloadPriority> priorities = m_contentModel->model()->getFilePriorities();
468  Q_ASSERT(priorities.size() == m_torrentInfo.filesCount());
469  for (int i = 0; i < priorities.size(); ++i)
470  {
471  if (priorities[i] > BitTorrent::DownloadPriority::Ignored)
472  torrentSize += m_torrentInfo.fileSize(i);
473  }
474  }
475  else
476  {
477  torrentSize = m_torrentInfo.totalSize();
478  }
479  }
480 
481  const QString sizeString = tr("%1 (Free space on disk: %2)").arg(
482  ((torrentSize > 0) ? Utils::Misc::friendlyUnit(torrentSize) : tr("Not available", "This size is unavailable."))
483  , Utils::Misc::friendlyUnit(Utils::Fs::freeDiskSpaceOnPath(m_ui->savePath->selectedPath())));
484  m_ui->labelSizeData->setText(sizeString);
485 }
486 
487 void AddNewTorrentDialog::onSavePathChanged(const QString &newPath)
488 {
489  Q_UNUSED(newPath);
490  // Remember index
491  m_savePathIndex = m_ui->savePath->currentIndex();
493 }
494 
495 void AddNewTorrentDialog::onDownloadPathChanged(const QString &newPath)
496 {
497  Q_UNUSED(newPath);
498  // Remember index
499  const int currentPathIndex = m_ui->downloadPath->currentIndex();
500  if (currentPathIndex >= 0)
501  m_downloadPathIndex = m_ui->downloadPath->currentIndex();
502 }
503 
505 {
506  m_useDownloadPath = checked;
507  m_ui->downloadPath->setCurrentIndex(checked ? m_downloadPathIndex : -1);
508 }
509 
511 {
512  Q_UNUSED(index);
513 
514  if (m_ui->comboTTM->currentIndex() == 1)
515  {
516  const auto *btSession = BitTorrent::Session::instance();
517  const QString categoryName = m_ui->categoryComboBox->currentText();
518 
519  const QString savePath = btSession->categorySavePath(categoryName);
520  m_ui->savePath->setSelectedPath(Utils::Fs::toNativePath(savePath));
521 
522  const QString downloadPath = btSession->categoryDownloadPath(categoryName);
523  m_ui->downloadPath->setSelectedPath(Utils::Fs::toNativePath(downloadPath));
524 
525  m_ui->groupBoxDownloadPath->setChecked(!m_ui->downloadPath->selectedPath().isEmpty());
526 
528  }
529 }
530 
532 {
533  if (!hasMetadata())
534  return;
535 
536  const auto filePriorities = m_contentModel->model()->getFilePriorities();
537  m_contentModel->model()->clear();
538 
539  Q_ASSERT(!m_torrentParams.filePaths.isEmpty());
540  const auto contentLayout = ((index == 0)
542  : static_cast<BitTorrent::TorrentContentLayout>(index));
545  m_contentModel->model()->updateFilesPriorities(filePriorities);
546 
547  // Expand single-item folders recursively
548  QModelIndex currentIndex;
549  while (m_contentModel->rowCount(currentIndex) == 1)
550  {
551  currentIndex = m_contentModel->index(0, 0, currentIndex);
552  m_ui->contentTreeView->setExpanded(currentIndex, true);
553  }
554 }
555 
557 {
558  Q_ASSERT(hasMetadata());
559 
560  const QString torrentFileExtension {C_TORRENT_FILE_EXTENSION};
561  const QString filter {tr("Torrent file (*%1)").arg(torrentFileExtension)};
562 
563  QString path = QFileDialog::getSaveFileName(
564  this, tr("Save as torrent file")
565  , QDir::home().absoluteFilePath(m_torrentInfo.name() + torrentFileExtension)
566  , filter);
567  if (path.isEmpty()) return;
568 
569  if (!path.endsWith(torrentFileExtension, Qt::CaseInsensitive))
570  path += torrentFileExtension;
571 
572  const nonstd::expected<void, QString> result = m_torrentInfo.saveToFile(path);
573  if (!result)
574  {
575  QMessageBox::critical(this, tr("I/O Error")
576  , tr("Couldn't export torrent metadata file '%1'. Reason: %2.").arg(path, result.error()));
577  }
578 }
579 
581 {
582  return m_torrentInfo.isValid();
583 }
584 
586 {
587  const auto *btSession = BitTorrent::Session::instance();
588 
589  m_ui->savePath->blockSignals(true);
590  m_ui->savePath->clear();
591  const auto savePathHistory = settings()->loadValue<QStringList>(KEY_SAVEPATHHISTORY);
592  if (savePathHistory.size() > 0)
593  {
594  for (const QString &path : savePathHistory)
595  m_ui->savePath->addItem(path);
596  }
597  else
598  {
599  m_ui->savePath->addItem(btSession->savePath());
600  }
601 
602  if (m_savePathIndex >= 0)
603  {
604  m_ui->savePath->setCurrentIndex(std::min(m_savePathIndex, (m_ui->savePath->count() - 1)));
605  }
606  else
607  {
608  if (!m_torrentParams.savePath.isEmpty())
609  setPath(m_ui->savePath, m_torrentParams.savePath);
610  else if (!m_storeRememberLastSavePath)
611  setPath(m_ui->savePath, btSession->savePath());
612  else
613  m_ui->savePath->setCurrentIndex(0);
614 
615  m_savePathIndex = m_ui->savePath->currentIndex();
616  }
617 
618  m_ui->savePath->blockSignals(false);
619 
620  m_ui->downloadPath->blockSignals(true);
621  m_ui->downloadPath->clear();
622  const auto downloadPathHistory = settings()->loadValue<QStringList>(KEY_DOWNLOADPATHHISTORY);
623  if (downloadPathHistory.size() > 0)
624  {
625  for (const QString &path : downloadPathHistory)
626  m_ui->downloadPath->addItem(path);
627  }
628  else
629  {
630  m_ui->downloadPath->addItem(btSession->downloadPath());
631  }
632 
633  if (m_downloadPathIndex >= 0)
634  {
635  m_ui->downloadPath->setCurrentIndex(m_useDownloadPath ? std::min(m_downloadPathIndex, (m_ui->downloadPath->count() - 1)) : -1);
636  m_ui->groupBoxDownloadPath->setChecked(m_useDownloadPath);
637  }
638  else
639  {
640  const bool useDownloadPath = m_torrentParams.useDownloadPath.value_or(btSession->isDownloadPathEnabled());
641  m_ui->groupBoxDownloadPath->setChecked(useDownloadPath);
642 
643  if (!m_torrentParams.downloadPath.isEmpty())
644  setPath(m_ui->downloadPath, m_torrentParams.downloadPath);
645  else if (!m_storeRememberLastSavePath)
646  setPath(m_ui->downloadPath, btSession->downloadPath());
647  else
648  m_ui->downloadPath->setCurrentIndex(0);
649 
650  m_downloadPathIndex = m_ui->downloadPath->currentIndex();
651  if (!useDownloadPath)
652  m_ui->downloadPath->setCurrentIndex(-1);
653  }
654 
655  m_ui->downloadPath->blockSignals(false);
656  m_ui->groupBoxDownloadPath->blockSignals(false);
657 }
658 
660 {
661  const QModelIndexList selectedRows = m_ui->contentTreeView->selectionModel()->selectedRows(0);
662 
663  const auto applyPriorities = [this](const BitTorrent::DownloadPriority prio)
664  {
665  const QModelIndexList selectedRows = m_ui->contentTreeView->selectionModel()->selectedRows(0);
666  for (const QModelIndex &index : selectedRows)
667  {
668  m_contentModel->setData(index.sibling(index.row(), PRIORITY)
669  , static_cast<int>(prio));
670  }
671  };
672  const auto applyPrioritiesByOrder = [this]()
673  {
674  // Equally distribute the selected items into groups and for each group assign
675  // a download priority that will apply to each item. The number of groups depends on how
676  // many "download priority" are available to be assigned
677 
678  const QModelIndexList selectedRows = m_ui->contentTreeView->selectionModel()->selectedRows(0);
679 
680  const qsizetype priorityGroups = 3;
681  const auto priorityGroupSize = std::max<qsizetype>((selectedRows.length() / priorityGroups), 1);
682 
683  for (qsizetype i = 0; i < selectedRows.length(); ++i)
684  {
686  switch (i / priorityGroupSize)
687  {
688  case 0:
690  break;
691  case 1:
693  break;
694  default:
695  case 2:
697  break;
698  }
699 
700  const QModelIndex &index = selectedRows[i];
701  m_contentModel->setData(index.sibling(index.row(), PRIORITY)
702  , static_cast<int>(priority));
703  }
704  };
705 
706  QMenu *menu = new QMenu(this);
707  menu->setAttribute(Qt::WA_DeleteOnClose);
708  if (selectedRows.size() == 1)
709  {
710  menu->addAction(UIThemeManager::instance()->getIcon("edit-rename"), tr("Rename..."), this, &AddNewTorrentDialog::renameSelectedFile);
711  menu->addSeparator();
712 
713  QMenu *priorityMenu = menu->addMenu(tr("Priority"));
714  priorityMenu->addAction(tr("Do not download"), priorityMenu, [applyPriorities]()
715  {
716  applyPriorities(BitTorrent::DownloadPriority::Ignored);
717  });
718  priorityMenu->addAction(tr("Normal"), priorityMenu, [applyPriorities]()
719  {
720  applyPriorities(BitTorrent::DownloadPriority::Normal);
721  });
722  priorityMenu->addAction(tr("High"), priorityMenu, [applyPriorities]()
723  {
724  applyPriorities(BitTorrent::DownloadPriority::High);
725  });
726  priorityMenu->addAction(tr("Maximum"), priorityMenu, [applyPriorities]()
727  {
728  applyPriorities(BitTorrent::DownloadPriority::Maximum);
729  });
730  priorityMenu->addSeparator();
731  priorityMenu->addAction(tr("By shown file order"), priorityMenu, applyPrioritiesByOrder);
732  }
733  else
734  {
735  menu->addAction(tr("Do not download"), menu, [applyPriorities]()
736  {
737  applyPriorities(BitTorrent::DownloadPriority::Ignored);
738  });
739  menu->addAction(tr("Normal priority"), menu, [applyPriorities]()
740  {
741  applyPriorities(BitTorrent::DownloadPriority::Normal);
742  });
743  menu->addAction(tr("High priority"), menu, [applyPriorities]()
744  {
745  applyPriorities(BitTorrent::DownloadPriority::High);
746  });
747  menu->addAction(tr("Maximum priority"), menu, [applyPriorities]()
748  {
749  applyPriorities(BitTorrent::DownloadPriority::Maximum);
750  });
751  menu->addSeparator();
752  menu->addAction(tr("Priority by shown file order"), menu, applyPrioritiesByOrder);
753  }
754 
755  menu->popup(QCursor::pos());
756 }
757 
759 {
760  // TODO: Check if destination actually exists
761  m_torrentParams.skipChecking = m_ui->skipCheckingCheckBox->isChecked();
762 
763  // Category
764  m_torrentParams.category = m_ui->categoryComboBox->currentText();
765  if (m_ui->defaultCategoryCheckbox->isChecked())
767 
768  m_storeRememberLastSavePath = m_ui->checkBoxRememberLastSavePath->isChecked();
769 
770  // Save file priorities
771  if (m_contentModel)
773 
774  m_torrentParams.addPaused = !m_ui->startTorrentCheckBox->isChecked();
775  m_torrentParams.contentLayout = static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex());
776 
777  m_torrentParams.sequential = m_ui->sequentialCheckBox->isChecked();
778  m_torrentParams.firstLastPiecePriority = m_ui->firstLastCheckBox->isChecked();
779 
780  const bool useAutoTMM = (m_ui->comboTTM->currentIndex() == 1); // 1 is Automatic mode. Handle all non 1 values as manual mode.
781  m_torrentParams.useAutoTMM = useAutoTMM;
782  if (!useAutoTMM)
783  {
784  const QString savePath = m_ui->savePath->selectedPath();
785  m_torrentParams.savePath = savePath;
787 
788  m_torrentParams.useDownloadPath = m_ui->groupBoxDownloadPath->isChecked();
790  {
791  const QString downloadPath = m_ui->downloadPath->selectedPath();
792  m_torrentParams.downloadPath = downloadPath;
794  }
795  }
796 
797  setEnabled(!m_ui->checkBoxNeverShow->isChecked());
798 
799  // Add torrent
800  if (!hasMetadata())
802  else
804 
805  m_torrentGuard->markAsAddedToSession();
806  QDialog::accept();
807 }
808 
810 {
811  if (!hasMetadata())
812  {
815  }
816 
817  QDialog::reject();
818 }
819 
821 {
822  Q_ASSERT(metadata.isValid());
823 
824  if (metadata.infoHash() != m_magnetURI.infoHash()) return;
825 
827 
828  // Good to go
829  m_torrentInfo = metadata;
830  setMetadataProgressIndicator(true, tr("Parsing metadata..."));
831 
832  // Update UI
833  setupTreeview();
834  setMetadataProgressIndicator(false, tr("Metadata retrieval complete"));
835 
836  m_ui->buttonSave->setVisible(true);
837  if (m_torrentInfo.infoHash().v2().isValid())
838  {
839  m_ui->buttonSave->setEnabled(false);
840  m_ui->buttonSave->setToolTip(tr("Cannot create v2 torrent until its data is fully downloaded."));
841  }
842 }
843 
844 void AddNewTorrentDialog::setMetadataProgressIndicator(bool visibleIndicator, const QString &labelText)
845 {
846  // Always show info label when waiting for metadata
847  m_ui->lblMetaLoading->setVisible(true);
848  m_ui->lblMetaLoading->setText(labelText);
849  m_ui->progMetaLoading->setVisible(visibleIndicator);
850 }
851 
853 {
854  if (!hasMetadata())
855  {
856  m_ui->labelCommentData->setText(tr("Not Available", "This comment is unavailable"));
857  m_ui->labelDateData->setText(tr("Not Available", "This date is unavailable"));
858  }
859  else
860  {
861  // Set dialog title
862  setWindowTitle(m_torrentInfo.name());
863 
864  // Set torrent information
865  m_ui->labelCommentData->setText(Utils::Misc::parseHtmlLinks(m_torrentInfo.comment().toHtmlEscaped()));
866  m_ui->labelDateData->setText(!m_torrentInfo.creationDate().isNull() ? QLocale().toString(m_torrentInfo.creationDate(), QLocale::ShortFormat) : tr("Not available"));
867 
868  // Prepare content tree
871  m_ui->contentTreeView->setModel(m_contentModel);
872  m_contentDelegate = new PropListDelegate(nullptr);
873  m_ui->contentTreeView->setItemDelegate(m_contentDelegate);
874  connect(m_ui->contentTreeView, &QAbstractItemView::clicked, m_ui->contentTreeView
875  , qOverload<const QModelIndex &>(&QAbstractItemView::edit));
876  connect(m_ui->contentTreeView, &QWidget::customContextMenuRequested, this, &AddNewTorrentDialog::displayContentTreeMenu);
877 
878  const auto contentLayout = ((m_ui->contentLayoutComboBox->currentIndex() == 0)
880  : static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex()));
881  if (m_torrentParams.filePaths.isEmpty())
884  // List files in torrent
886  if (const QByteArray state = m_storeTreeHeaderState; !state.isEmpty())
887  m_ui->contentTreeView->header()->restoreState(state);
888 
889  // Hide useless columns after loading the header state
890  m_ui->contentTreeView->hideColumn(PROGRESS);
891  m_ui->contentTreeView->hideColumn(REMAINING);
892  m_ui->contentTreeView->hideColumn(AVAILABILITY);
893 
894  // Expand single-item folders recursively
895  QModelIndex currentIndex;
896  while (m_contentModel->rowCount(currentIndex) == 1)
897  {
898  currentIndex = m_contentModel->index(0, 0, currentIndex);
899  m_ui->contentTreeView->setExpanded(currentIndex, true);
900  }
901  }
902 
904 }
905 
907 {
908  switch (downloadResult.status)
909  {
911  {
912  const nonstd::expected<BitTorrent::TorrentInfo, QString> result = BitTorrent::TorrentInfo::load(downloadResult.data);
913  if (!result)
914  {
915  RaisedMessageBox::critical(this, tr("Invalid torrent"), tr("Failed to load from URL: %1.\nError: %2")
916  .arg(downloadResult.url, result.error()));
917  return;
918  }
919 
920  m_torrentInfo = result.value();
921  m_torrentGuard = std::make_unique<TorrentFileGuard>();
922 
923  if (loadTorrentImpl())
924  open();
925  else
926  deleteLater();
927  }
928  break;
930  if (loadMagnet(BitTorrent::MagnetUri(downloadResult.magnet)))
931  open();
932  else
933  deleteLater();
934  break;
935  default:
936  RaisedMessageBox::critical(this, tr("Download Error"),
937  tr("Cannot download '%1': %2").arg(downloadResult.url, downloadResult.errorString));
938  deleteLater();
939  }
940 }
941 
943 {
944  if (index != 1)
945  { // 0 is Manual mode and 1 is Automatic mode. Handle all non 1 values as manual mode.
947  m_ui->groupBoxSavePath->setEnabled(true);
948  }
949  else
950  {
951  const auto *session = BitTorrent::Session::instance();
952 
953  m_ui->groupBoxSavePath->setEnabled(false);
954 
955  m_ui->savePath->blockSignals(true);
956  m_ui->savePath->clear();
957  const QString savePath = session->categorySavePath(m_ui->categoryComboBox->currentText());
958  m_ui->savePath->addItem(savePath);
959 
960  m_ui->downloadPath->blockSignals(true);
961  m_ui->downloadPath->clear();
962  const QString downloadPath = session->categoryDownloadPath(m_ui->categoryComboBox->currentText());
963  m_ui->downloadPath->addItem(downloadPath);
964 
965  m_ui->groupBoxDownloadPath->blockSignals(true);
966  m_ui->groupBoxDownloadPath->setChecked(!downloadPath.isEmpty());
967 
969  }
970 }
971 
973 {
974  m_torrentGuard->setAutoRemove(!checked);
975 }
976 
978 {
979  if (hasMetadata())
980  {
981  FileStorageAdaptor fileStorageAdaptor {m_torrentInfo, m_torrentParams.filePaths};
982  m_ui->contentTreeView->renameSelectedFile(fileStorageAdaptor);
983  }
984 }
QBITTORRENT_HAS_EXECINFO_H if(NOT QBITTORRENT_HAS_EXECINFO_H) message(FATAL_ERROR "execinfo.h header file not found.\n" "Please either disable the STACKTRACE feature or use a libc that has this header file
#define SETTINGS_KEY(name)
static void setSavePathHistoryLength(int value)
void showEvent(QShowEvent *event) override
void onDownloadPathChanged(const QString &newPath)
bool loadTorrentFile(const QString &torrentPath)
AddNewTorrentDialog(const BitTorrent::AddTorrentParams &inParams, QWidget *parent)
bool loadMagnet(const BitTorrent::MagnetUri &magnetUri)
static const int minPathHistoryLength
void doNotDeleteTorrentClicked(bool checked)
void categoryChanged(int index)
static void setEnabled(bool value)
BitTorrent::MagnetUri m_magnetURI
BitTorrent::AddTorrentParams m_torrentParams
SettingValue< QSize > m_storeDialogSize
TorrentContentFilterModel * m_contentModel
void setMetadataProgressIndicator(bool visibleIndicator, const QString &labelText={})
void contentLayoutChanged(int index)
static void show(const QString &source, const BitTorrent::AddTorrentParams &inParams, QWidget *parent)
SettingValue< QByteArray > m_storeTreeHeaderState
void displayContentTreeMenu(const QPoint &)
SettingValue< bool > m_storeRememberLastSavePath
static const int maxPathHistoryLength
void updateMetadata(const BitTorrent::TorrentInfo &metadata)
static int savePathHistoryLength()
void onUseDownloadPathChanged(bool checked)
static void setTopLevel(bool value)
Ui::AddNewTorrentDialog * m_ui
void onSavePathChanged(const QString &newPath)
SettingValue< QString > m_storeDefaultCategory
void handleDownloadFinished(const Net::DownloadResult &downloadResult)
PropListDelegate * m_contentDelegate
std::unique_ptr< TorrentFileGuard > m_torrentGuard
SettingValue< QByteArray > m_storeSplitterState
BitTorrent::TorrentInfo m_torrentInfo
SHA1Hash v1() const
Definition: infohash.cpp:44
SHA256Hash v2() const
Definition: infohash.cpp:53
TorrentID toTorrentID() const
Definition: infohash.cpp:62
QVector< TrackerEntry > trackers() const
Definition: magneturi.cpp:125
bool isValid() const
Definition: magneturi.cpp:110
InfoHash infoHash() const
Definition: magneturi.cpp:115
QString name() const
Definition: magneturi.cpp:120
QVector< QUrl > urlSeeds() const
Definition: magneturi.cpp:130
bool cancelDownloadMetadata(const TorrentID &id)
Definition: session.cpp:1850
static Session * instance()
Definition: session.cpp:997
void metadataDownloaded(const TorrentInfo &info)
bool addTorrent(const QString &source, const AddTorrentParams &params=AddTorrentParams())
Definition: session.cpp:2007
bool downloadMetadata(const MagnetUri &magnetUri)
Definition: session.cpp:2283
Torrent * findTorrent(const TorrentID &id) const
Definition: session.cpp:1742
virtual void addUrlSeeds(const QVector< QUrl > &urlSeeds)=0
virtual void addTrackers(const QVector< TrackerEntry > &trackers)=0
virtual bool isPrivate() const =0
virtual QString name() const =0
static TorrentID fromInfoHash(const InfoHash &infoHash)
Definition: infohash.cpp:81
QVector< QUrl > urlSeeds() const
static nonstd::expected< TorrentInfo, QString > load(const QByteArray &data) noexcept
Definition: torrentinfo.cpp:89
qlonglong totalSize() const
QVector< TrackerEntry > trackers() const
QString comment() const
InfoHash infoHash() const
QStringList filePaths() const
QDateTime creationDate() const
QString name() const
qlonglong fileSize(int index) const
nonstd::expected< void, QString > saveToFile(const QString &path) const
static nonstd::expected< TorrentInfo, QString > loadFromFile(const QString &path) noexcept
QString toString() const
Definition: digest32.h:85
bool isValid() const
Definition: digest32.h:58
Widget which uses QComboBox for path editing.
Definition: fspathedit.h:130
QString item(int index) const
Definition: fspathedit.cpp:364
void insertItem(int index, const QString &text)
Definition: fspathedit.cpp:374
void setCurrentIndex(int index)
Definition: fspathedit.cpp:384
@ DirectorySave
selecting directories for saving
void selectedPathChanged(const QString &path)
static bool hasSupportedScheme(const QString &url)
DownloadHandler * download(const DownloadRequest &downloadRequest)
static DownloadManager * instance()
static QMessageBox::StandardButton information(QWidget *parent, const QString &title, const QString &text, QMessageBox::StandardButtons buttons=QMessageBox::Ok, QMessageBox::StandardButton defaultButton=QMessageBox::NoButton)
static QMessageBox::StandardButton critical(QWidget *parent, const QString &title, const QString &text, QMessageBox::StandardButtons buttons=QMessageBox::Ok, QMessageBox::StandardButton defaultButton=QMessageBox::NoButton)
static QMessageBox::StandardButton warning(QWidget *parent, const QString &title, const QString &text, QMessageBox::StandardButtons buttons=QMessageBox::Ok, QMessageBox::StandardButton defaultButton=QMessageBox::NoButton)
void storeValue(const QString &key, const T &value)
T loadValue(const QString &key, const T &defaultValue={}) const
static SettingsStorage * instance()
TorrentContentModel * model() const
void updateFilesPriorities(const QVector< BitTorrent::DownloadPriority > &fprio)
QVector< BitTorrent::DownloadPriority > getFilePriorities() const
void setupModelData(const BitTorrent::AbstractFileStorage &info)
static AutoDeleteMode autoDeleteMode()
static UIThemeManager * instance()
FileStorageAdaptor(const BitTorrent::TorrentInfo &torrentInfo, QStringList &filePaths)
void renameFile(const int index, const QString &newFilePath) override
const char C_TORRENT_FILE_EXTENSION[]
Definition: global.h:38
const int MAX_TORRENT_SIZE
Definition: global.h:39
constexpr std::add_const_t< T > & asConst(T &t) noexcept
Definition: global.h:42
TorrentContentLayout detectContentLayout(const QStringList &filePaths)
void applyContentLayout(QStringList &filePaths, TorrentContentLayout contentLayout, const QString &rootFolder={})
qint64 freeDiskSpaceOnPath(const QString &path)
Definition: fs.cpp:271
QString findRootFolder(const QStringList &filePaths)
Definition: fs.cpp:403
QString toNativePath(const QString &path)
Definition: fs.cpp:64
void resize(QWidget *widget, const QSize &newSize={})
Definition: utils.cpp:54
QString parseHtmlLinks(const QString &rawText)
Definition: misc.cpp:404
QString friendlyUnit(qint64 bytes, bool isSpeed=false)
Definition: misc.cpp:261
int indexOfPath(const FileSystemPathComboEdit *fsPathEdit, const QString &savePath)
void updatePathHistory(const QString &settingsKey, const QString &path, const int maxLength)
void setPath(FileSystemPathComboEdit *fsPathEdit, const QString &newPath)
T value(const QString &key, const T &defaultValue={})
Definition: preferences.cpp:64
QString toString(const lt::socket_type_t socketType)
Definition: session.cpp:183
@ PRIORITY
@ AVAILABILITY
@ REMAINING
@ PROGRESS
QVector< DownloadPriority > filePriorities
std::optional< bool > useDownloadPath
std::optional< bool > useAutoTMM
std::optional< bool > addPaused
std::optional< BitTorrent::TorrentContentLayout > contentLayout
DownloadStatus status