1 /*
2 * This file Copyright (C) 2009-2015 Mnemosyne LLC
3 *
4 * It may be used under the GNU GPL versions 2 or 3
5 * or any future license endorsed by Mnemosyne LLC.
6 *
7 */
8
9 #include <algorithm> // std::any_of
10 #include <cassert>
11 #include <climits> /* INT_MAX */
12 #include <ctime>
13
14 #include <QDateTime>
15 #include <QDesktopServices>
16 #include <QEvent>
17 #include <QFont>
18 #include <QFontMetrics>
19 #include <QHeaderView>
20 #include <QHostAddress>
21 #include <QInputDialog>
22 #include <QItemSelectionModel>
23 #include <QLabel>
24 #include <QList>
25 #include <QMap>
26 #include <QMessageBox>
27 #include <QResizeEvent>
28 #include <QStringList>
29 #include <QStyle>
30 #include <QTreeWidgetItem>
31
32 #include <libtransmission/transmission.h>
33 #include <libtransmission/utils.h> // tr_getRatio()
34
35 #include "ColumnResizer.h"
36 #include "DetailsDialog.h"
37 #include "Formatter.h"
38 #include "Prefs.h"
39 #include "Session.h"
40 #include "SqueezeLabel.h"
41 #include "Torrent.h"
42 #include "TorrentModel.h"
43 #include "TrackerDelegate.h"
44 #include "TrackerModel.h"
45 #include "TrackerModelFilter.h"
46 #include "Utils.h"
47
48 class Prefs;
49 class Session;
50
51 /****
52 *****
53 ****/
54
55 namespace
56 {
57
58 int const REFRESH_INTERVAL_MSEC = 4000;
59
60 char const* PREF_KEY("pref-key");
61
62 enum // peer columns
63 {
64 COL_LOCK,
65 COL_UP,
66 COL_DOWN,
67 COL_PERCENT,
68 COL_STATUS,
69 COL_ADDRESS,
70 COL_CLIENT,
71 N_COLUMNS
72 };
73
measureViewItem(QTreeWidget * view,int column,QString const & text)74 int measureViewItem(QTreeWidget* view, int column, QString const& text)
75 {
76 QTreeWidgetItem const* headerItem = view->headerItem();
77
78 int const itemWidth = Utils::measureViewItem(view, text);
79 int const headerWidth = Utils::measureHeaderItem(view->header(), headerItem->text(column));
80
81 return std::max(itemWidth, headerWidth);
82 }
83
84 } // namespace
85
86 /***
87 ****
88 ***/
89
90 class PeerItem : public QTreeWidgetItem
91 {
92 Peer peer;
93 QString mutable collatedAddress;
94 QString status;
95
96 public:
PeerItem(Peer const & p)97 PeerItem(Peer const& p) :
98 peer(p)
99 {
100 }
101
~PeerItem()102 virtual ~PeerItem()
103 {
104 }
105
106 public:
refresh(Peer const & p)107 void refresh(Peer const& p)
108 {
109 if (p.address != peer.address)
110 {
111 collatedAddress.clear();
112 }
113
114 peer = p;
115 }
116
setStatus(QString const & s)117 void setStatus(QString const& s)
118 {
119 status = s;
120 }
121
operator <(QTreeWidgetItem const & other) const122 virtual bool operator <(QTreeWidgetItem const& other) const
123 {
124 PeerItem const* i = dynamic_cast<PeerItem const*>(&other);
125 QTreeWidget* tw(treeWidget());
126 int const column = tw != nullptr ? tw->sortColumn() : 0;
127
128 assert(i != nullptr);
129
130 switch (column)
131 {
132 case COL_UP:
133 return peer.rateToPeer < i->peer.rateToPeer;
134
135 case COL_DOWN:
136 return peer.rateToClient < i->peer.rateToClient;
137
138 case COL_PERCENT:
139 return peer.progress < i->peer.progress;
140
141 case COL_STATUS:
142 return status < i->status;
143
144 case COL_CLIENT:
145 return peer.clientName < i->peer.clientName;
146
147 case COL_LOCK:
148 return peer.isEncrypted && !i->peer.isEncrypted;
149
150 default:
151 return address() < i->address();
152 }
153 }
154
155 private:
address() const156 QString const& address() const
157 {
158 if (collatedAddress.isEmpty())
159 {
160 QHostAddress ipAddress;
161
162 if (ipAddress.setAddress(peer.address))
163 {
164 if (ipAddress.protocol() == QAbstractSocket::IPv4Protocol)
165 {
166 quint32 const ipv4Address = ipAddress.toIPv4Address();
167 collatedAddress = QLatin1String("1-") + QString::fromLatin1(QByteArray::number(ipv4Address, 16).
168 rightJustified(8, '0'));
169 }
170 else if (ipAddress.protocol() == QAbstractSocket::IPv6Protocol)
171 {
172 Q_IPV6ADDR const ipv6Address = ipAddress.toIPv6Address();
173 QByteArray tmp(16, '\0');
174
175 for (int i = 0; i < 16; ++i)
176 {
177 tmp[i] = ipv6Address[i];
178 }
179
180 collatedAddress = QLatin1String("2-") + QString::fromLatin1(tmp.toHex());
181 }
182 }
183
184 if (collatedAddress.isEmpty())
185 {
186 collatedAddress = QLatin1String("3-") + peer.address.toLower();
187 }
188 }
189
190 return collatedAddress;
191 }
192 };
193
194 /***
195 ****
196 ***/
197
getStockIcon(QString const & freedesktop_name,int fallback)198 QIcon DetailsDialog::getStockIcon(QString const& freedesktop_name, int fallback)
199 {
200 QIcon icon = QIcon::fromTheme(freedesktop_name);
201
202 if (icon.isNull())
203 {
204 icon = style()->standardIcon(QStyle::StandardPixmap(fallback), nullptr, this);
205 }
206
207 return icon;
208 }
209
DetailsDialog(Session & session,Prefs & prefs,TorrentModel const & model,QWidget * parent)210 DetailsDialog::DetailsDialog(Session& session, Prefs& prefs, TorrentModel const& model, QWidget* parent) :
211 BaseDialog(parent),
212 mySession(session),
213 myPrefs(prefs),
214 myModel(model),
215 myChangedTorrents(false),
216 myHavePendingRefresh(false)
217 {
218 ui.setupUi(this);
219
220 initInfoTab();
221 initPeersTab();
222 initTrackerTab();
223 initFilesTab();
224 initOptionsTab();
225
226 adjustSize();
227 ui.commentBrowser->setMaximumHeight(QWIDGETSIZE_MAX);
228
229 QList<int> initKeys;
230 initKeys << Prefs::SHOW_TRACKER_SCRAPES << Prefs::SHOW_BACKUP_TRACKERS;
231
232 for (int const key : initKeys)
233 {
234 refreshPref(key);
235 }
236
237 connect(&myModel, &TorrentModel::torrentsChanged, this, &DetailsDialog::onTorrentsChanged);
238 connect(&myPrefs, &Prefs::changed, this, &DetailsDialog::refreshPref);
239 connect(&myTimer, &QTimer::timeout, this, &DetailsDialog::onTimer);
240
241 onTimer();
242 myTimer.setSingleShot(false);
243 myTimer.start(REFRESH_INTERVAL_MSEC);
244 }
245
~DetailsDialog()246 DetailsDialog::~DetailsDialog()
247 {
248 myTrackerDelegate->deleteLater();
249 myTrackerFilter->deleteLater();
250 myTrackerModel->deleteLater();
251 }
252
setIds(torrent_ids_t const & ids)253 void DetailsDialog::setIds(torrent_ids_t const& ids)
254 {
255 if (ids != myIds)
256 {
257 setEnabled(false);
258 ui.filesView->clear();
259
260 myIds = ids;
261 mySession.refreshDetailInfo(myIds);
262 myChangedTorrents = true;
263 myTrackerModel->refresh(myModel, myIds);
264 onTimer();
265 }
266 }
267
refreshPref(int key)268 void DetailsDialog::refreshPref(int key)
269 {
270 QString str;
271
272 switch (key)
273 {
274 case Prefs::SHOW_TRACKER_SCRAPES:
275 {
276 QItemSelectionModel* selectionModel(ui.trackersView->selectionModel());
277 QItemSelection const selection(selectionModel->selection());
278 QModelIndex const currentIndex(selectionModel->currentIndex());
279 myTrackerDelegate->setShowMore(myPrefs.getBool(key));
280 selectionModel->clear();
281 ui.trackersView->reset();
282 selectionModel->select(selection, QItemSelectionModel::Select);
283 selectionModel->setCurrentIndex(currentIndex, QItemSelectionModel::NoUpdate);
284 break;
285 }
286
287 case Prefs::SHOW_BACKUP_TRACKERS:
288 myTrackerFilter->setShowBackupTrackers(myPrefs.getBool(key));
289 break;
290
291 default:
292 break;
293 }
294 }
295
296 /***
297 ****
298 ***/
299
onTimer()300 void DetailsDialog::onTimer()
301 {
302 getNewData();
303 }
304
getNewData()305 void DetailsDialog::getNewData()
306 {
307 if (!myIds.empty())
308 {
309 mySession.refreshExtraStats(myIds);
310 }
311 }
312
onTorrentEdited(torrent_ids_t const &)313 void DetailsDialog::onTorrentEdited(torrent_ids_t const& /*ids*/)
314 {
315 // FIXME
316 // refreshDetailInfo({ tor.id() });
317 }
318
onTorrentsChanged(torrent_ids_t const & ids)319 void DetailsDialog::onTorrentsChanged(torrent_ids_t const& ids)
320 {
321 if (myHavePendingRefresh)
322 {
323 return;
324 }
325
326 if (!std::any_of(ids.begin(), ids.end(), [this](auto const& id) { return myIds.count(id) != 0; }))
327 {
328 return;
329 }
330
331 myHavePendingRefresh = true;
332 QTimer::singleShot(100, this, SLOT(refresh()));
333 }
334
335 namespace
336 {
337
setIfIdle(QComboBox * box,int i)338 void setIfIdle(QComboBox* box, int i)
339 {
340 if (!box->hasFocus())
341 {
342 box->blockSignals(true);
343 box->setCurrentIndex(i);
344 box->blockSignals(false);
345 }
346 }
347
setIfIdle(QDoubleSpinBox * spin,double value)348 void setIfIdle(QDoubleSpinBox* spin, double value)
349 {
350 if (!spin->hasFocus())
351 {
352 spin->blockSignals(true);
353 spin->setValue(value);
354 spin->blockSignals(false);
355 }
356 }
357
setIfIdle(QSpinBox * spin,int value)358 void setIfIdle(QSpinBox* spin, int value)
359 {
360 if (!spin->hasFocus())
361 {
362 spin->blockSignals(true);
363 spin->setValue(value);
364 spin->blockSignals(false);
365 }
366 }
367
368 } // namespace
369
refresh()370 void DetailsDialog::refresh()
371 {
372 int const n = myIds.size();
373 bool const single = n == 1;
374 QString const blank;
375 QFontMetrics const fm(fontMetrics());
376 QList<Torrent const*> torrents;
377 QString string;
378 QString const none = tr("None");
379 QString const mixed = tr("Mixed");
380 QString const unknown = tr("Unknown");
381
382 // build a list of torrents
383 for (int const id : myIds)
384 {
385 Torrent const* tor = myModel.getTorrentFromId(id);
386
387 if (tor != nullptr)
388 {
389 torrents << tor;
390 }
391 }
392
393 ///
394 /// activity tab
395 ///
396
397 // myStateLabel
398 if (torrents.empty())
399 {
400 string = none;
401 }
402 else
403 {
404 bool isMixed = false;
405 bool allPaused = true;
406 bool allFinished = true;
407 tr_torrent_activity const baseline = torrents[0]->getActivity();
408
409 for (Torrent const* const t : torrents)
410 {
411 tr_torrent_activity const activity = t->getActivity();
412
413 if (activity != baseline)
414 {
415 isMixed = true;
416 }
417
418 if (activity != TR_STATUS_STOPPED)
419 {
420 allPaused = allFinished = false;
421 }
422
423 if (!t->isFinished())
424 {
425 allFinished = false;
426 }
427 }
428
429 if (isMixed)
430 {
431 string = mixed;
432 }
433 else if (allFinished)
434 {
435 string = tr("Finished");
436 }
437 else if (allPaused)
438 {
439 string = tr("Paused");
440 }
441 else
442 {
443 string = torrents[0]->activityString();
444 }
445 }
446
447 ui.stateValueLabel->setText(string);
448 QString const stateString = string;
449
450 // myHaveLabel
451 uint64_t sizeWhenDone = 0;
452 uint64_t available = 0;
453
454 if (torrents.empty())
455 {
456 string = none;
457 }
458 else
459 {
460 uint64_t leftUntilDone = 0;
461 int64_t haveTotal = 0;
462 int64_t haveVerified = 0;
463 int64_t haveUnverified = 0;
464 int64_t verifiedPieces = 0;
465
466 for (Torrent const* const t : torrents)
467 {
468 if (t->hasMetadata())
469 {
470 haveTotal += t->haveTotal();
471 haveUnverified += t->haveUnverified();
472 uint64_t const v = t->haveVerified();
473 haveVerified += v;
474
475 if (t->pieceSize())
476 {
477 verifiedPieces += v / t->pieceSize();
478 }
479
480 sizeWhenDone += t->sizeWhenDone();
481 leftUntilDone += t->leftUntilDone();
482 available += t->sizeWhenDone() - t->leftUntilDone() + t->desiredAvailable();
483 }
484 }
485
486 double const d = sizeWhenDone != 0 ? 100.0 * (sizeWhenDone - leftUntilDone) / sizeWhenDone : 100.0;
487 QString pct = Formatter::percentToString(d);
488
489 if (haveUnverified == 0 && leftUntilDone == 0)
490 {
491 //: Text following the "Have:" label in torrent properties dialog;
492 //: %1 is amount of downloaded and verified data
493 string = tr("%1 (100%)").arg(Formatter::sizeToString(haveVerified));
494 }
495 else if (haveUnverified == 0)
496 {
497 //: Text following the "Have:" label in torrent properties dialog;
498 //: %1 is amount of downloaded and verified data,
499 //: %2 is overall size of torrent data,
500 //: %3 is percentage (%1/%2*100)
501 string = tr("%1 of %2 (%3%)").arg(Formatter::sizeToString(haveVerified)).arg(Formatter::sizeToString(sizeWhenDone)).
502 arg(pct);
503 }
504 else
505 {
506 //: Text following the "Have:" label in torrent properties dialog;
507 //: %1 is amount of downloaded data (both verified and unverified),
508 //: %2 is overall size of torrent data,
509 //: %3 is percentage (%1/%2*100),
510 //: %4 is amount of downloaded but not yet verified data
511 string = tr("%1 of %2 (%3%), %4 Unverified").arg(Formatter::sizeToString(haveVerified + haveUnverified)).
512 arg(Formatter::sizeToString(sizeWhenDone)).arg(pct).arg(Formatter::sizeToString(haveUnverified));
513 }
514 }
515
516 ui.haveValueLabel->setText(string);
517
518 // myAvailabilityLabel
519 if (torrents.empty())
520 {
521 string = none;
522 }
523 else if (sizeWhenDone == 0)
524 {
525 string = none;
526 }
527 else
528 {
529 string = QString::fromLatin1("%1%").arg(Formatter::percentToString((100.0 * available) / sizeWhenDone));
530 }
531
532 ui.availabilityValueLabel->setText(string);
533
534 // myDownloadedLabel
535 if (torrents.empty())
536 {
537 string = none;
538 }
539 else
540 {
541 uint64_t d = 0;
542 uint64_t f = 0;
543
544 for (Torrent const* const t : torrents)
545 {
546 d += t->downloadedEver();
547 f += t->failedEver();
548 }
549
550 QString const dstr = Formatter::sizeToString(d);
551 QString const fstr = Formatter::sizeToString(f);
552
553 if (f != 0)
554 {
555 string = tr("%1 (%2 corrupt)").arg(dstr).arg(fstr);
556 }
557 else
558 {
559 string = dstr;
560 }
561 }
562
563 ui.downloadedValueLabel->setText(string);
564
565 // myUploadedLabel
566 if (torrents.empty())
567 {
568 string = none;
569 }
570 else
571 {
572 uint64_t u = 0;
573 uint64_t d = 0;
574
575 for (Torrent const* const t : torrents)
576 {
577 u += t->uploadedEver();
578 d += t->downloadedEver();
579 }
580
581 string = tr("%1 (Ratio: %2)").arg(Formatter::sizeToString(u)).arg(Formatter::ratioToString(tr_getRatio(u, d)));
582 }
583
584 ui.uploadedValueLabel->setText(string);
585
586 // myRunTimeLabel
587 if (torrents.empty())
588 {
589 string = none;
590 }
591 else
592 {
593 bool allPaused = true;
594 auto baseline = torrents[0]->lastStarted();
595
596 for (Torrent const* const t : torrents)
597 {
598 if (baseline != t->lastStarted())
599 {
600 baseline = 0;
601 }
602
603 if (!t->isPaused())
604 {
605 allPaused = false;
606 }
607 }
608
609 if (allPaused)
610 {
611 string = stateString; // paused || finished
612 }
613 else if (baseline == 0)
614 {
615 string = mixed;
616 }
617 else
618 {
619 auto const now = time(nullptr);
620 auto const seconds = int(std::difftime(now, baseline));
621 string = Formatter::timeToString(seconds);
622 }
623 }
624
625 ui.runningTimeValueLabel->setText(string);
626
627 // myETALabel
628 string.clear();
629
630 if (torrents.empty())
631 {
632 string = none;
633 }
634 else
635 {
636 int baseline = torrents[0]->getETA();
637
638 for (Torrent const* const t : torrents)
639 {
640 if (baseline != t->getETA())
641 {
642 string = mixed;
643 break;
644 }
645 }
646
647 if (string.isEmpty())
648 {
649 if (baseline < 0)
650 {
651 string = tr("Unknown");
652 }
653 else
654 {
655 string = Formatter::timeToString(baseline);
656 }
657 }
658 }
659
660 ui.remainingTimeValueLabel->setText(string);
661
662 // myLastActivityLabel
663 if (torrents.empty())
664 {
665 string = none;
666 }
667 else
668 {
669 auto latest = torrents[0]->lastActivity();
670
671 for (Torrent const* const t : torrents)
672 {
673 auto const dt = t->lastActivity();
674
675 if (latest < dt)
676 {
677 latest = dt;
678 }
679 }
680
681 auto const now = time(nullptr);
682 auto const seconds = int(std::difftime(now, latest));
683
684 if (seconds < 0)
685 {
686 string = none;
687 }
688 else if (seconds < 5)
689 {
690 string = tr("Active now");
691 }
692 else
693 {
694 string = tr("%1 ago").arg(Formatter::timeToString(seconds));
695 }
696 }
697
698 ui.lastActivityValueLabel->setText(string);
699
700 if (torrents.empty())
701 {
702 string = none;
703 }
704 else
705 {
706 string = torrents[0]->getError();
707
708 for (Torrent const* const t : torrents)
709 {
710 if (string != t->getError())
711 {
712 string = mixed;
713 break;
714 }
715 }
716 }
717
718 if (string.isEmpty())
719 {
720 string = none;
721 }
722
723 ui.errorValueLabel->setText(string);
724
725 ///
726 /// information tab
727 ///
728
729 // mySizeLabel
730 if (torrents.empty())
731 {
732 string = none;
733 }
734 else
735 {
736 int pieces = 0;
737 uint64_t size = 0;
738 uint32_t pieceSize = torrents[0]->pieceSize();
739
740 for (Torrent const* const t : torrents)
741 {
742 pieces += t->pieceCount();
743 size += t->totalSize();
744
745 if (pieceSize != t->pieceSize())
746 {
747 pieceSize = 0;
748 }
749 }
750
751 if (size == 0)
752 {
753 string = none;
754 }
755 else if (pieceSize > 0)
756 {
757 string = tr("%1 (%Ln pieces @ %2)", "", pieces).arg(Formatter::sizeToString(size)).
758 arg(Formatter::memToString(pieceSize));
759 }
760 else
761 {
762 string = tr("%1 (%Ln pieces)", "", pieces).arg(Formatter::sizeToString(size));
763 }
764 }
765
766 ui.sizeValueLabel->setText(string);
767
768 // myHashLabel
769 string = none;
770
771 if (!torrents.empty())
772 {
773 string = torrents[0]->hashString();
774
775 for (Torrent const* const t : torrents)
776 {
777 if (string != t->hashString())
778 {
779 string = mixed;
780 break;
781 }
782 }
783 }
784
785 ui.hashValueLabel->setText(string);
786
787 // myPrivacyLabel
788 string = none;
789
790 if (!torrents.empty())
791 {
792 bool b = torrents[0]->isPrivate();
793 string = b ? tr("Private to this tracker -- DHT and PEX disabled") : tr("Public torrent");
794
795 for (Torrent const* const t : torrents)
796 {
797 if (b != t->isPrivate())
798 {
799 string = mixed;
800 break;
801 }
802 }
803 }
804
805 ui.privacyValueLabel->setText(string);
806
807 // myCommentBrowser
808 string = none;
809 bool isCommentMixed = false;
810
811 if (!torrents.empty())
812 {
813 string = torrents[0]->comment();
814
815 for (Torrent const* const t : torrents)
816 {
817 if (string != t->comment())
818 {
819 string = mixed;
820 isCommentMixed = true;
821 break;
822 }
823 }
824 }
825
826 if (ui.commentBrowser->toPlainText() != string)
827 {
828 ui.commentBrowser->setText(string);
829 }
830
831 ui.commentBrowser->setEnabled(!isCommentMixed && !string.isEmpty());
832
833 // myOriginLabel
834 string = none;
835
836 if (!torrents.empty())
837 {
838 bool mixed_creator = false;
839 bool mixed_date = false;
840 QString const creator = torrents[0]->creator();
841 auto const date = torrents[0]->dateCreated();
842
843 for (Torrent const* const t : torrents)
844 {
845 mixed_creator |= (creator != t->creator());
846 mixed_date |= (date != t->dateCreated());
847 }
848
849 bool const empty_creator = creator.isEmpty();
850 bool const empty_date = date <= 0;
851
852 if (mixed_creator || mixed_date)
853 {
854 string = mixed;
855 }
856 else if (empty_creator && empty_date)
857 {
858 string = tr("N/A");
859 }
860 else if (empty_date && !empty_creator)
861 {
862 string = tr("Created by %1").arg(creator);
863 }
864 else if (empty_creator && !empty_date)
865 {
866 auto const dateStr = QDateTime::fromSecsSinceEpoch(date).toString();
867 string = tr("Created on %1").arg(dateStr);
868 }
869 else
870 {
871 auto const dateStr = QDateTime::fromSecsSinceEpoch(date).toString();
872 string = tr("Created by %1 on %2").arg(creator).arg(dateStr);
873 }
874 }
875
876 ui.originValueLabel->setText(string);
877
878 // myLocationLabel
879 string = none;
880
881 if (!torrents.empty())
882 {
883 string = torrents[0]->getPath();
884
885 for (Torrent const* const t : torrents)
886 {
887 if (string != t->getPath())
888 {
889 string = mixed;
890 break;
891 }
892 }
893 }
894
895 ui.locationValueLabel->setText(string);
896
897 ///
898 /// Options Tab
899 ///
900
901 if (myChangedTorrents && !torrents.empty())
902 {
903 int i;
904 bool uniform;
905 bool baselineFlag;
906 int baselineInt;
907 Torrent const& baseline = *torrents.front();
908
909 // mySessionLimitCheck
910 uniform = true;
911 baselineFlag = baseline.honorsSessionLimits();
912
913 for (Torrent const* const tor : torrents)
914 {
915 if (baselineFlag != tor->honorsSessionLimits())
916 {
917 uniform = false;
918 break;
919 }
920 }
921
922 ui.sessionLimitCheck->setChecked(uniform && baselineFlag);
923
924 // mySingleDownCheck
925 uniform = true;
926 baselineFlag = baseline.downloadIsLimited();
927
928 for (Torrent const* const tor : torrents)
929 {
930 if (baselineFlag != tor->downloadIsLimited())
931 {
932 uniform = false;
933 break;
934 }
935 }
936
937 ui.singleDownCheck->setChecked(uniform && baselineFlag);
938
939 // mySingleUpCheck
940 uniform = true;
941 baselineFlag = baseline.uploadIsLimited();
942
943 for (Torrent const* const tor : torrents)
944 {
945 if (baselineFlag != tor->uploadIsLimited())
946 {
947 uniform = false;
948 break;
949 }
950 }
951
952 ui.singleUpCheck->setChecked(uniform && baselineFlag);
953
954 // myBandwidthPriorityCombo
955 uniform = true;
956 baselineInt = baseline.getBandwidthPriority();
957
958 for (Torrent const* const tor : torrents)
959 {
960 if (baselineInt != tor->getBandwidthPriority())
961 {
962 uniform = false;
963 break;
964 }
965 }
966
967 if (uniform)
968 {
969 i = ui.bandwidthPriorityCombo->findData(baselineInt);
970 }
971 else
972 {
973 i = -1;
974 }
975
976 setIfIdle(ui.bandwidthPriorityCombo, i);
977
978 setIfIdle(ui.singleDownSpin, int(baseline.downloadLimit().KBps()));
979 setIfIdle(ui.singleUpSpin, int(baseline.uploadLimit().KBps()));
980 setIfIdle(ui.peerLimitSpin, baseline.peerLimit());
981 }
982
983 if (!torrents.empty())
984 {
985 Torrent const& baseline = *torrents.front();
986
987 // ratio
988 bool uniform = true;
989 int baselineInt = baseline.seedRatioMode();
990
991 for (Torrent const* const tor : torrents)
992 {
993 if (baselineInt != tor->seedRatioMode())
994 {
995 uniform = false;
996 break;
997 }
998 }
999
1000 setIfIdle(ui.ratioCombo, uniform ? ui.ratioCombo->findData(baselineInt) : -1);
1001 ui.ratioSpin->setVisible(uniform && baselineInt == TR_RATIOLIMIT_SINGLE);
1002
1003 setIfIdle(ui.ratioSpin, baseline.seedRatioLimit());
1004
1005 // idle
1006 uniform = true;
1007 baselineInt = baseline.seedIdleMode();
1008
1009 for (Torrent const* const tor : torrents)
1010 {
1011 if (baselineInt != tor->seedIdleMode())
1012 {
1013 uniform = false;
1014 break;
1015 }
1016 }
1017
1018 setIfIdle(ui.idleCombo, uniform ? ui.idleCombo->findData(baselineInt) : -1);
1019 ui.idleSpin->setVisible(uniform && baselineInt == TR_RATIOLIMIT_SINGLE);
1020
1021 setIfIdle(ui.idleSpin, baseline.seedIdleLimit());
1022 onIdleLimitChanged();
1023 }
1024
1025 ///
1026 /// Tracker tab
1027 ///
1028
1029 myTrackerModel->refresh(myModel, myIds);
1030
1031 ///
1032 /// Peers tab
1033 ///
1034
1035 QMap<QString, QTreeWidgetItem*> peers2;
1036 QList<QTreeWidgetItem*> newItems;
1037
1038 for (Torrent const* const t : torrents)
1039 {
1040 QString const idStr(QString::number(t->id()));
1041 PeerList peers = t->peers();
1042
1043 for (Peer const& peer : peers)
1044 {
1045 QString const key = idStr + QLatin1Char(':') + peer.address;
1046 PeerItem* item = static_cast<PeerItem*>(myPeers.value(key, nullptr));
1047
1048 if (item == nullptr) // new peer has connected
1049 {
1050 static QIcon const myEncryptionIcon(QLatin1String(":/icons/encrypted.png"));
1051 static QIcon const myEmptyIcon;
1052 item = new PeerItem(peer);
1053 item->setTextAlignment(COL_UP, Qt::AlignRight | Qt::AlignVCenter);
1054 item->setTextAlignment(COL_DOWN, Qt::AlignRight | Qt::AlignVCenter);
1055 item->setTextAlignment(COL_PERCENT, Qt::AlignRight | Qt::AlignVCenter);
1056 item->setIcon(COL_LOCK, peer.isEncrypted ? myEncryptionIcon : myEmptyIcon);
1057 item->setToolTip(COL_LOCK, peer.isEncrypted ? tr("Encrypted connection") : QString());
1058 item->setText(COL_ADDRESS, peer.address);
1059 item->setText(COL_CLIENT, peer.clientName);
1060 newItems << item;
1061 }
1062
1063 QString const code = peer.flagStr;
1064 item->setStatus(code);
1065 item->refresh(peer);
1066
1067 QString codeTip;
1068
1069 for (QChar const ch : code)
1070 {
1071 QString txt;
1072
1073 switch (ch.unicode())
1074 {
1075 case 'O':
1076 txt = tr("Optimistic unchoke");
1077 break;
1078
1079 case 'D':
1080 txt = tr("Downloading from this peer");
1081 break;
1082
1083 case 'd':
1084 txt = tr("We would download from this peer if they would let us");
1085 break;
1086
1087 case 'U':
1088 txt = tr("Uploading to peer");
1089 break;
1090
1091 case 'u':
1092 txt = tr("We would upload to this peer if they asked");
1093 break;
1094
1095 case 'K':
1096 txt = tr("Peer has unchoked us, but we're not interested");
1097 break;
1098
1099 case '?':
1100 txt = tr("We unchoked this peer, but they're not interested");
1101 break;
1102
1103 case 'E':
1104 txt = tr("Encrypted connection");
1105 break;
1106
1107 case 'H':
1108 txt = tr("Peer was discovered through DHT");
1109 break;
1110
1111 case 'X':
1112 txt = tr("Peer was discovered through Peer Exchange (PEX)");
1113 break;
1114
1115 case 'I':
1116 txt = tr("Peer is an incoming connection");
1117 break;
1118
1119 case 'T':
1120 txt = tr("Peer is connected over uTP");
1121 break;
1122 }
1123
1124 if (!txt.isEmpty())
1125 {
1126 codeTip += QString::fromLatin1("%1: %2\n").arg(ch).arg(txt);
1127 }
1128 }
1129
1130 if (!codeTip.isEmpty())
1131 {
1132 codeTip.resize(codeTip.size() - 1); // eat the trailing linefeed
1133 }
1134
1135 item->setText(COL_UP, peer.rateToPeer.isZero() ? QString() : Formatter::speedToString(peer.rateToPeer));
1136 item->setText(COL_DOWN, peer.rateToClient.isZero() ? QString() : Formatter::speedToString(peer.rateToClient));
1137 item->setText(COL_PERCENT, peer.progress > 0 ? QString::fromLatin1("%1%").arg(int(peer.progress * 100.0)) :
1138 QString());
1139 item->setText(COL_STATUS, code);
1140 item->setToolTip(COL_STATUS, codeTip);
1141
1142 peers2.insert(key, item);
1143 }
1144 }
1145
1146 ui.peersView->addTopLevelItems(newItems);
1147
1148 for (QString const& key : myPeers.keys())
1149 {
1150 if (!peers2.contains(key)) // old peer has disconnected
1151 {
1152 QTreeWidgetItem* item = myPeers.value(key, nullptr);
1153 ui.peersView->takeTopLevelItem(ui.peersView->indexOfTopLevelItem(item));
1154 delete item;
1155 }
1156 }
1157
1158 myPeers = peers2;
1159
1160 if (!single)
1161 {
1162 ui.filesView->clear();
1163 }
1164
1165 if (single)
1166 {
1167 ui.filesView->update(torrents[0]->files(), myChangedTorrents);
1168 }
1169
1170 myChangedTorrents = false;
1171 myHavePendingRefresh = false;
1172 setEnabled(true);
1173 }
1174
setEnabled(bool enabled)1175 void DetailsDialog::setEnabled(bool enabled)
1176 {
1177 for (int i = 0; i < ui.tabs->count(); ++i)
1178 {
1179 ui.tabs->widget(i)->setEnabled(enabled);
1180 }
1181 }
1182
1183 /***
1184 ****
1185 ***/
1186
initInfoTab()1187 void DetailsDialog::initInfoTab()
1188 {
1189 int const h = QFontMetrics(ui.commentBrowser->font()).lineSpacing() * 4;
1190 ui.commentBrowser->setFixedHeight(h);
1191
1192 ColumnResizer* cr(new ColumnResizer(this));
1193 cr->addLayout(ui.activitySectionLayout);
1194 cr->addLayout(ui.detailsSectionLayout);
1195 cr->update();
1196 }
1197
1198 /***
1199 ****
1200 ***/
1201
onShowTrackerScrapesToggled(bool val)1202 void DetailsDialog::onShowTrackerScrapesToggled(bool val)
1203 {
1204 myPrefs.set(Prefs::SHOW_TRACKER_SCRAPES, val);
1205 }
1206
onShowBackupTrackersToggled(bool val)1207 void DetailsDialog::onShowBackupTrackersToggled(bool val)
1208 {
1209 myPrefs.set(Prefs::SHOW_BACKUP_TRACKERS, val);
1210 }
1211
onHonorsSessionLimitsToggled(bool val)1212 void DetailsDialog::onHonorsSessionLimitsToggled(bool val)
1213 {
1214 mySession.torrentSet(myIds, TR_KEY_honorsSessionLimits, val);
1215 getNewData();
1216 }
1217
onDownloadLimitedToggled(bool val)1218 void DetailsDialog::onDownloadLimitedToggled(bool val)
1219 {
1220 mySession.torrentSet(myIds, TR_KEY_downloadLimited, val);
1221 getNewData();
1222 }
1223
onSpinBoxEditingFinished()1224 void DetailsDialog::onSpinBoxEditingFinished()
1225 {
1226 QObject const* spin = sender();
1227 tr_quark const key = spin->property(PREF_KEY).toInt();
1228 QDoubleSpinBox const* d = qobject_cast<QDoubleSpinBox const*>(spin);
1229
1230 if (d != nullptr)
1231 {
1232 mySession.torrentSet(myIds, key, d->value());
1233 }
1234 else
1235 {
1236 mySession.torrentSet(myIds, key, qobject_cast<QSpinBox const*>(spin)->value());
1237 }
1238
1239 getNewData();
1240 }
1241
onUploadLimitedToggled(bool val)1242 void DetailsDialog::onUploadLimitedToggled(bool val)
1243 {
1244 mySession.torrentSet(myIds, TR_KEY_uploadLimited, val);
1245 getNewData();
1246 }
1247
onIdleModeChanged(int index)1248 void DetailsDialog::onIdleModeChanged(int index)
1249 {
1250 int const val = ui.idleCombo->itemData(index).toInt();
1251 mySession.torrentSet(myIds, TR_KEY_seedIdleMode, val);
1252 getNewData();
1253 }
1254
onIdleLimitChanged()1255 void DetailsDialog::onIdleLimitChanged()
1256 {
1257 //: Spin box suffix, "Stop seeding if idle for: [ 5 minutes ]" (includes leading space after the number, if needed)
1258 QString const unitsSuffix = tr(" minute(s)", nullptr, ui.idleSpin->value());
1259
1260 if (ui.idleSpin->suffix() != unitsSuffix)
1261 {
1262 ui.idleSpin->setSuffix(unitsSuffix);
1263 }
1264 }
1265
onRatioModeChanged(int index)1266 void DetailsDialog::onRatioModeChanged(int index)
1267 {
1268 int const val = ui.ratioCombo->itemData(index).toInt();
1269 mySession.torrentSet(myIds, TR_KEY_seedRatioMode, val);
1270 }
1271
onBandwidthPriorityChanged(int index)1272 void DetailsDialog::onBandwidthPriorityChanged(int index)
1273 {
1274 if (index != -1)
1275 {
1276 int const priority = ui.bandwidthPriorityCombo->itemData(index).toInt();
1277 mySession.torrentSet(myIds, TR_KEY_bandwidthPriority, priority);
1278 getNewData();
1279 }
1280 }
1281
onTrackerSelectionChanged()1282 void DetailsDialog::onTrackerSelectionChanged()
1283 {
1284 int const selectionCount = ui.trackersView->selectionModel()->selectedRows().size();
1285 ui.editTrackerButton->setEnabled(selectionCount == 1);
1286 ui.removeTrackerButton->setEnabled(selectionCount > 0);
1287 }
1288
onAddTrackerClicked()1289 void DetailsDialog::onAddTrackerClicked()
1290 {
1291 bool ok = false;
1292 QString const url = QInputDialog::getText(this, tr("Add URL "), tr("Add tracker announce URL:"), QLineEdit::Normal,
1293 QString(), &ok);
1294
1295 if (!ok)
1296 {
1297 // user pressed "cancel" -- noop
1298 }
1299 else if (!QUrl(url).isValid())
1300 {
1301 QMessageBox::warning(this, tr("Error"), tr("Invalid URL \"%1\"").arg(url));
1302 }
1303 else
1304 {
1305 torrent_ids_t ids;
1306
1307 for (int const id : myIds)
1308 {
1309 if (myTrackerModel->find(id, url) == -1)
1310 {
1311 ids.insert(id);
1312 }
1313 }
1314
1315 if (ids.empty()) // all the torrents already have this tracker
1316 {
1317 QMessageBox::warning(this, tr("Error"), tr("Tracker already exists."));
1318 }
1319 else
1320 {
1321 QStringList urls;
1322 urls << url;
1323 mySession.torrentSet(ids, TR_KEY_trackerAdd, urls);
1324 getNewData();
1325 }
1326 }
1327 }
1328
onEditTrackerClicked()1329 void DetailsDialog::onEditTrackerClicked()
1330 {
1331 QItemSelectionModel* selectionModel = ui.trackersView->selectionModel();
1332 QModelIndexList selectedRows = selectionModel->selectedRows();
1333 assert(selectedRows.size() == 1);
1334 QModelIndex i = selectionModel->currentIndex();
1335 TrackerInfo const trackerInfo = ui.trackersView->model()->data(i, TrackerModel::TrackerRole).value<TrackerInfo>();
1336
1337 bool ok = false;
1338 QString const newval = QInputDialog::getText(this, tr("Edit URL "), tr("Edit tracker announce URL:"), QLineEdit::Normal,
1339 trackerInfo.st.announce, &ok);
1340
1341 if (!ok)
1342 {
1343 // user pressed "cancel" -- noop
1344 }
1345 else if (!QUrl(newval).isValid())
1346 {
1347 QMessageBox::warning(this, tr("Error"), tr("Invalid URL \"%1\"").arg(newval));
1348 }
1349 else
1350 {
1351 torrent_ids_t ids{ trackerInfo.torrentId };
1352
1353 QPair<int, QString> const idUrl = qMakePair(trackerInfo.st.id, newval);
1354
1355 mySession.torrentSet(ids, TR_KEY_trackerReplace, idUrl);
1356 getNewData();
1357 }
1358 }
1359
onRemoveTrackerClicked()1360 void DetailsDialog::onRemoveTrackerClicked()
1361 {
1362 // make a map of torrentIds to announce URLs to remove
1363 QItemSelectionModel* selectionModel = ui.trackersView->selectionModel();
1364 QModelIndexList selectedRows = selectionModel->selectedRows();
1365 QMap<int, int> torrentId_to_trackerIds;
1366
1367 for (QModelIndex const& i : selectedRows)
1368 {
1369 TrackerInfo const inf = ui.trackersView->model()->data(i, TrackerModel::TrackerRole).value<TrackerInfo>();
1370 torrentId_to_trackerIds.insertMulti(inf.torrentId, inf.st.id);
1371 }
1372
1373 // batch all of a tracker's torrents into one command
1374 for (int const id : torrentId_to_trackerIds.uniqueKeys())
1375 {
1376 torrent_ids_t const ids{ id };
1377 mySession.torrentSet(ids, TR_KEY_trackerRemove, torrentId_to_trackerIds.values(id));
1378 }
1379
1380 selectionModel->clearSelection();
1381 getNewData();
1382 }
1383
initOptionsTab()1384 void DetailsDialog::initOptionsTab()
1385 {
1386 QString const speed_K_str = Formatter::unitStr(Formatter::SPEED, Formatter::KB);
1387
1388 ui.singleDownSpin->setSuffix(QString::fromLatin1(" %1").arg(speed_K_str));
1389 ui.singleUpSpin->setSuffix(QString::fromLatin1(" %1").arg(speed_K_str));
1390
1391 ui.singleDownSpin->setProperty(PREF_KEY, TR_KEY_downloadLimit);
1392 ui.singleUpSpin->setProperty(PREF_KEY, TR_KEY_uploadLimit);
1393 ui.ratioSpin->setProperty(PREF_KEY, TR_KEY_seedRatioLimit);
1394 ui.idleSpin->setProperty(PREF_KEY, TR_KEY_seedIdleLimit);
1395 ui.peerLimitSpin->setProperty(PREF_KEY, TR_KEY_peer_limit);
1396
1397 ui.bandwidthPriorityCombo->addItem(tr("High"), TR_PRI_HIGH);
1398 ui.bandwidthPriorityCombo->addItem(tr("Normal"), TR_PRI_NORMAL);
1399 ui.bandwidthPriorityCombo->addItem(tr("Low"), TR_PRI_LOW);
1400
1401 ui.ratioCombo->addItem(tr("Use Global Settings"), TR_RATIOLIMIT_GLOBAL);
1402 ui.ratioCombo->addItem(tr("Seed regardless of ratio"), TR_RATIOLIMIT_UNLIMITED);
1403 ui.ratioCombo->addItem(tr("Stop seeding at ratio:"), TR_RATIOLIMIT_SINGLE);
1404
1405 ui.idleCombo->addItem(tr("Use Global Settings"), TR_IDLELIMIT_GLOBAL);
1406 ui.idleCombo->addItem(tr("Seed regardless of activity"), TR_IDLELIMIT_UNLIMITED);
1407 ui.idleCombo->addItem(tr("Stop seeding if idle for:"), TR_IDLELIMIT_SINGLE);
1408
1409 ColumnResizer* cr(new ColumnResizer(this));
1410 cr->addLayout(ui.speedSectionLayout);
1411 cr->addLayout(ui.seedingLimitsSectionRatioLayout);
1412 cr->addLayout(ui.seedingLimitsSectionIdleLayout);
1413 cr->addLayout(ui.peerConnectionsSectionLayout);
1414 cr->update();
1415
1416 void (QComboBox::* comboIndexChanged)(int) = &QComboBox::currentIndexChanged;
1417 void (QSpinBox::* spinValueChanged)(int) = &QSpinBox::valueChanged;
1418 connect(ui.bandwidthPriorityCombo, comboIndexChanged, this, &DetailsDialog::onBandwidthPriorityChanged);
1419 connect(ui.idleCombo, comboIndexChanged, this, &DetailsDialog::onIdleModeChanged);
1420 connect(ui.idleSpin, &QSpinBox::editingFinished, this, &DetailsDialog::onSpinBoxEditingFinished);
1421 connect(ui.idleSpin, spinValueChanged, this, &DetailsDialog::onIdleLimitChanged);
1422 connect(ui.peerLimitSpin, &QSpinBox::editingFinished, this, &DetailsDialog::onSpinBoxEditingFinished);
1423 connect(ui.ratioCombo, comboIndexChanged, this, &DetailsDialog::onRatioModeChanged);
1424 connect(ui.ratioSpin, &QSpinBox::editingFinished, this, &DetailsDialog::onSpinBoxEditingFinished);
1425 connect(ui.sessionLimitCheck, &QCheckBox::clicked, this, &DetailsDialog::onHonorsSessionLimitsToggled);
1426 connect(ui.singleDownCheck, &QCheckBox::clicked, this, &DetailsDialog::onDownloadLimitedToggled);
1427 connect(ui.singleDownSpin, &QSpinBox::editingFinished, this, &DetailsDialog::onSpinBoxEditingFinished);
1428 connect(ui.singleUpCheck, &QCheckBox::clicked, this, &DetailsDialog::onUploadLimitedToggled);
1429 connect(ui.singleUpSpin, &QSpinBox::editingFinished, this, &DetailsDialog::onSpinBoxEditingFinished);
1430 }
1431
1432 /***
1433 ****
1434 ***/
1435
initTrackerTab()1436 void DetailsDialog::initTrackerTab()
1437 {
1438 myTrackerModel = new TrackerModel();
1439 myTrackerFilter = new TrackerModelFilter();
1440 myTrackerFilter->setSourceModel(myTrackerModel);
1441 myTrackerDelegate = new TrackerDelegate();
1442
1443 ui.trackersView->setModel(myTrackerFilter);
1444 ui.trackersView->setItemDelegate(myTrackerDelegate);
1445
1446 ui.addTrackerButton->setIcon(getStockIcon(QLatin1String("list-add"), QStyle::SP_DialogOpenButton));
1447 ui.editTrackerButton->setIcon(getStockIcon(QLatin1String("document-properties"), QStyle::SP_DesktopIcon));
1448 ui.removeTrackerButton->setIcon(getStockIcon(QLatin1String("list-remove"), QStyle::SP_TrashIcon));
1449
1450 ui.showTrackerScrapesCheck->setChecked(myPrefs.getBool(Prefs::SHOW_TRACKER_SCRAPES));
1451 ui.showBackupTrackersCheck->setChecked(myPrefs.getBool(Prefs::SHOW_BACKUP_TRACKERS));
1452
1453 connect(ui.addTrackerButton, &QAbstractButton::clicked, this, &DetailsDialog::onAddTrackerClicked);
1454 connect(ui.editTrackerButton, &QAbstractButton::clicked, this, &DetailsDialog::onEditTrackerClicked);
1455 connect(ui.removeTrackerButton, &QAbstractButton::clicked, this, &DetailsDialog::onRemoveTrackerClicked);
1456 connect(ui.showBackupTrackersCheck, &QAbstractButton::clicked, this, &DetailsDialog::onShowBackupTrackersToggled);
1457 connect(ui.showTrackerScrapesCheck, &QAbstractButton::clicked, this, &DetailsDialog::onShowTrackerScrapesToggled);
1458 connect(
1459 ui.trackersView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
1460 &DetailsDialog::onTrackerSelectionChanged);
1461
1462 onTrackerSelectionChanged();
1463 }
1464
1465 /***
1466 ****
1467 ***/
1468
initPeersTab()1469 void DetailsDialog::initPeersTab()
1470 {
1471 ui.peersView->setHeaderLabels({ QString(), tr("Up"), tr("Down"), tr("%"), tr("Status"), tr("Address"), tr("Client") });
1472 ui.peersView->sortByColumn(COL_ADDRESS, Qt::AscendingOrder);
1473
1474 ui.peersView->setColumnWidth(COL_LOCK, 20);
1475 ui.peersView->setColumnWidth(COL_UP, measureViewItem(ui.peersView, COL_UP, QLatin1String("1024 MiB/s")));
1476 ui.peersView->setColumnWidth(COL_DOWN, measureViewItem(ui.peersView, COL_DOWN, QLatin1String("1024 MiB/s")));
1477 ui.peersView->setColumnWidth(COL_PERCENT, measureViewItem(ui.peersView, COL_PERCENT, QLatin1String("100%")));
1478 ui.peersView->setColumnWidth(COL_STATUS, measureViewItem(ui.peersView, COL_STATUS, QLatin1String("ODUK?EXI")));
1479 ui.peersView->setColumnWidth(COL_ADDRESS, measureViewItem(ui.peersView, COL_ADDRESS, QLatin1String("888.888.888.888")));
1480 }
1481
1482 /***
1483 ****
1484 ***/
1485
initFilesTab()1486 void DetailsDialog::initFilesTab()
1487 {
1488 connect(ui.filesView, &FileTreeView::openRequested, this, &DetailsDialog::onOpenRequested);
1489 connect(ui.filesView, &FileTreeView::pathEdited, this, &DetailsDialog::onPathEdited);
1490 connect(ui.filesView, &FileTreeView::priorityChanged, this, &DetailsDialog::onFilePriorityChanged);
1491 connect(ui.filesView, &FileTreeView::wantedChanged, this, &DetailsDialog::onFileWantedChanged);
1492 }
1493
onFilePriorityChanged(QSet<int> const & indices,int priority)1494 void DetailsDialog::onFilePriorityChanged(QSet<int> const& indices, int priority)
1495 {
1496 tr_quark key;
1497
1498 switch (priority)
1499 {
1500 case TR_PRI_LOW:
1501 key = TR_KEY_priority_low;
1502 break;
1503
1504 case TR_PRI_HIGH:
1505 key = TR_KEY_priority_high;
1506 break;
1507
1508 default:
1509 key = TR_KEY_priority_normal;
1510 break;
1511 }
1512
1513 mySession.torrentSet(myIds, key, indices.toList());
1514 getNewData();
1515 }
1516
onFileWantedChanged(QSet<int> const & indices,bool wanted)1517 void DetailsDialog::onFileWantedChanged(QSet<int> const& indices, bool wanted)
1518 {
1519 tr_quark const key = wanted ? TR_KEY_files_wanted : TR_KEY_files_unwanted;
1520 mySession.torrentSet(myIds, key, indices.toList());
1521 getNewData();
1522 }
1523
onPathEdited(QString const & oldpath,QString const & newname)1524 void DetailsDialog::onPathEdited(QString const& oldpath, QString const& newname)
1525 {
1526 mySession.torrentRenamePath(myIds, oldpath, newname);
1527 }
1528
onOpenRequested(QString const & path)1529 void DetailsDialog::onOpenRequested(QString const& path)
1530 {
1531 if (!mySession.isLocal())
1532 {
1533 return;
1534 }
1535
1536 for (int const id : myIds)
1537 {
1538 Torrent const* const tor = myModel.getTorrentFromId(id);
1539
1540 if (tor == nullptr)
1541 {
1542 continue;
1543 }
1544
1545 QString const localFilePath = tor->getPath() + QLatin1Char('/') + path;
1546
1547 if (!QFile::exists(localFilePath))
1548 {
1549 continue;
1550 }
1551
1552 if (QDesktopServices::openUrl(QUrl::fromLocalFile(localFilePath)))
1553 {
1554 break;
1555 }
1556 }
1557 }
1558