2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2008 Christophe Dumez <chris@qbittorrent.org>
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.
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.
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.
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.
29 /* -----------------------------------------------------------------
31 ATTACH MOCHA LINK EVENTS
32 Notes: Here is where you define your windows and the events that open them.
33 If you are not using links to run Mocha methods you can remove this function.
35 If you need to add link events to links within windows you are creating, do
36 it in the onContentLoaded function of the new window.
38 ----------------------------------------------------------------- */
41 window
.qBittorrent
??= {};
42 window
.qBittorrent
.Dialog
??= (() => {
43 const exports
= () => {
45 baseModalOptions
: baseModalOptions
49 const deepFreeze
= (obj
) => {
50 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#examples
51 // accounts for circular refs
52 const frozen
= new WeakSet();
53 const deepFreezeSafe
= (obj
) => {
59 const keys
= Reflect
.ownKeys(obj
);
60 for (const key
of keys
) {
61 const value
= obj
[key
];
62 if ((value
&& (typeof value
=== "object")) || (typeof value
=== "function"))
63 deepFreezeSafe(value
);
71 const baseModalOptions
= Object
.assign(Object
.create(null), {
72 addClass
: "modalDialog",
77 icon
: "images/qbittorrent-tray.svg",
90 onCloseComplete
: () => {
91 // make sure overlay is properly hidden upon modal closing
92 document
.getElementById("modalOverlay").style
.display
= "none";
96 deepFreeze(baseModalOptions
);
100 Object
.freeze(window
.qBittorrent
.Dialog
);
102 const LocalPreferences
= new window
.qBittorrent
.LocalPreferences
.LocalPreferences();
104 let saveWindowSize
= () => {};
105 let loadWindowWidth
= () => {};
106 let loadWindowHeight
= () => {};
107 let showDownloadPage
= () => {};
108 let globalUploadLimitFN
= () => {};
109 let uploadLimitFN
= () => {};
110 let shareRatioFN
= () => {};
111 let toggleSequentialDownloadFN
= () => {};
112 let toggleFirstLastPiecePrioFN
= () => {};
113 let setSuperSeedingFN
= () => {};
114 let setForceStartFN
= () => {};
115 let globalDownloadLimitFN
= () => {};
116 let StatisticsLinkFN
= () => {};
117 let downloadLimitFN
= () => {};
118 let deleteSelectedTorrentsFN
= () => {};
119 let stopFN
= () => {};
120 let startFN
= () => {};
121 let autoTorrentManagementFN
= () => {};
122 let recheckFN
= () => {};
123 let reannounceFN
= () => {};
124 let setLocationFN
= () => {};
125 let renameFN
= () => {};
126 let renameFilesFN
= () => {};
127 let startVisibleTorrentsFN
= () => {};
128 let stopVisibleTorrentsFN
= () => {};
129 let deleteVisibleTorrentsFN
= () => {};
130 let torrentNewCategoryFN
= () => {};
131 let torrentSetCategoryFN
= () => {};
132 let createCategoryFN
= () => {};
133 let createSubcategoryFN
= () => {};
134 let editCategoryFN
= () => {};
135 let removeCategoryFN
= () => {};
136 let deleteUnusedCategoriesFN
= () => {};
137 let torrentAddTagsFN
= () => {};
138 let torrentSetTagsFN
= () => {};
139 let torrentRemoveAllTagsFN
= () => {};
140 let createTagFN
= () => {};
141 let removeTagFN
= () => {};
142 let deleteUnusedTagsFN
= () => {};
143 let deleteTrackerFN
= () => {};
144 let copyNameFN
= () => {};
145 let copyInfohashFN
= (policy
) => {};
146 let copyMagnetLinkFN
= () => {};
147 let copyIdFN
= () => {};
148 let copyCommentFN
= () => {};
149 let setQueuePositionFN
= () => {};
150 let exportTorrentFN
= () => {};
152 const initializeWindows
= () => {
153 saveWindowSize
= (windowId
) => {
154 const size
= $(windowId
).getSize();
155 LocalPreferences
.set("window_" + windowId
+ "_width", size
.x
);
156 LocalPreferences
.set("window_" + windowId
+ "_height", size
.y
);
159 loadWindowWidth
= (windowId
, defaultValue
) => {
160 return LocalPreferences
.get("window_" + windowId
+ "_width", defaultValue
);
163 loadWindowHeight
= (windowId
, defaultValue
) => {
164 return LocalPreferences
.get("window_" + windowId
+ "_height", defaultValue
);
167 const addClickEvent
= (el
, fn
) => {
168 ["Link", "Button"].each((item
) => {
170 $(el
+ item
).addEventListener("click", fn
);
174 addClickEvent("download", (e
) => {
180 showDownloadPage
= (urls
) => {
181 const id
= "downloadPage";
182 const contentUri
= new URI("download.html");
184 if (urls
&& (urls
.length
> 0))
185 contentUri
.setData("urls", urls
.map(encodeURIComponent
).join("|"));
189 icon
: "images/qbittorrent-tray.svg",
190 title
: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
191 loadMethod
: "iframe",
192 contentURL
: contentUri
.toString(),
193 addClass
: "windowFrame", // fixes iframe scrolling on iOS Safari
198 paddingHorizontal
: 0,
199 width
: loadWindowWidth(id
, 500),
200 height
: loadWindowHeight(id
, 600),
201 onResize
: window
.qBittorrent
.Misc
.createDebounceHandler(500, (e
) => {
208 addClickEvent("preferences", (e
) => {
212 const id
= "preferencesPage";
215 icon
: "images/qbittorrent-tray.svg",
216 title
: "QBT_TR(Options)QBT_TR[CONTEXT=OptionsDialog]",
219 contentURL
: new URI("views/preferences.html").toString(),
221 css
: ["css/Tabs.css"]
223 toolbarURL
: "views/preferencesToolbar.html",
227 paddingHorizontal
: 0,
228 width
: loadWindowWidth(id
, 730),
229 height
: loadWindowHeight(id
, 600),
230 onResize
: window
.qBittorrent
.Misc
.createDebounceHandler(500, (e
) => {
236 addClickEvent("manageCookies", (e
) => {
240 const id
= "cookiesPage";
243 title
: "QBT_TR(Manage Cookies)QBT_TR[CONTEXT=CookiesDialog]",
245 contentURL
: new URI("views/cookies.html").toString(),
248 paddingHorizontal
: 0,
249 width
: loadWindowWidth(id
, 900),
250 height
: loadWindowHeight(id
, 400),
257 addClickEvent("upload", (e
) => {
261 const id
= "uploadPage";
264 icon
: "images/qbittorrent-tray.svg",
265 title
: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
266 loadMethod
: "iframe",
267 contentURL
: new URI("upload.html").toString(),
268 addClass
: "windowFrame", // fixes iframe scrolling on iOS Safari
272 paddingHorizontal
: 0,
273 width
: loadWindowWidth(id
, 500),
274 height
: loadWindowHeight(id
, 460),
275 onResize
: window
.qBittorrent
.Misc
.createDebounceHandler(500, (e
) => {
282 globalUploadLimitFN
= () => {
284 id
: "uploadLimitPage",
285 icon
: "images/qbittorrent-tray.svg",
286 title
: "QBT_TR(Global Upload Speed Limit)QBT_TR[CONTEXT=MainWindow]",
287 loadMethod
: "iframe",
288 contentURL
: new URI("uploadlimit.html").setData("hashes", "global").toString(),
293 paddingHorizontal
: 0,
299 uploadLimitFN
= () => {
300 const hashes
= torrentsTable
.selectedRowsIds();
303 id
: "uploadLimitPage",
304 icon
: "images/qbittorrent-tray.svg",
305 title
: "QBT_TR(Torrent Upload Speed Limiting)QBT_TR[CONTEXT=TransferListWidget]",
306 loadMethod
: "iframe",
307 contentURL
: new URI("uploadlimit.html").setData("hashes", hashes
.join("|")).toString(),
312 paddingHorizontal
: 0,
319 shareRatioFN
= () => {
320 const hashes
= torrentsTable
.selectedRowsIds();
322 let shareRatio
= null;
323 let torrentsHaveSameShareRatio
= true;
325 // check if all selected torrents have same share ratio
326 for (let i
= 0; i
< hashes
.length
; ++i
) {
327 const hash
= hashes
[i
];
328 const row
= torrentsTable
.getRow(hash
).full_data
;
329 const origValues
= row
.ratio_limit
+ "|" + row
.seeding_time_limit
+ "|" + row
.inactive_seeding_time_limit
+ "|"
330 + row
.max_ratio
+ "|" + row
.max_seeding_time
+ "|" + row
.max_inactive_seeding_time
;
333 if (shareRatio
=== null)
334 shareRatio
= origValues
;
336 if (origValues
!== shareRatio
) {
337 torrentsHaveSameShareRatio
= false;
342 // if all torrents have same share ratio, display that share ratio. else use the default
343 const orig
= torrentsHaveSameShareRatio
? shareRatio
: "";
345 id
: "shareRatioPage",
346 icon
: "images/qbittorrent-tray.svg",
347 title
: "QBT_TR(Torrent Upload/Download Ratio Limiting)QBT_TR[CONTEXT=UpDownRatioDialog]",
348 loadMethod
: "iframe",
349 contentURL
: new URI("shareratio.html").setData("hashes", hashes
.join("|")).setData("orig", orig
).toString(),
353 paddingHorizontal
: 0,
360 toggleSequentialDownloadFN
= () => {
361 const hashes
= torrentsTable
.selectedRowsIds();
363 fetch("api/v2/torrents/toggleSequentialDownload", {
365 body
: new URLSearchParams({
366 hashes
: hashes
.join("|")
373 toggleFirstLastPiecePrioFN
= () => {
374 const hashes
= torrentsTable
.selectedRowsIds();
376 fetch("api/v2/torrents/toggleFirstLastPiecePrio", {
378 body
: new URLSearchParams({
379 hashes
: hashes
.join("|")
386 setSuperSeedingFN
= (val
) => {
387 const hashes
= torrentsTable
.selectedRowsIds();
389 fetch("api/v2/torrents/setSuperSeeding", {
391 body
: new URLSearchParams({
392 hashes
: hashes
.join("|"),
400 setForceStartFN
= () => {
401 const hashes
= torrentsTable
.selectedRowsIds();
403 fetch("api/v2/torrents/setForceStart", {
405 body
: new URLSearchParams({
406 hashes
: hashes
.join("|"),
414 globalDownloadLimitFN
= () => {
416 id
: "downloadLimitPage",
417 icon
: "images/qbittorrent-tray.svg",
418 title
: "QBT_TR(Global Download Speed Limit)QBT_TR[CONTEXT=MainWindow]",
419 loadMethod
: "iframe",
420 contentURL
: new URI("downloadlimit.html").setData("hashes", "global").toString(),
425 paddingHorizontal
: 0,
431 StatisticsLinkFN
= () => {
432 const id
= "statisticspage";
435 icon
: "images/qbittorrent-tray.svg",
436 title
: "QBT_TR(Statistics)QBT_TR[CONTEXT=StatsDialog]",
438 contentURL
: new URI("views/statistics.html").toString(),
441 width
: loadWindowWidth(id
, 285),
442 height
: loadWindowHeight(id
, 415),
443 onResize
: window
.qBittorrent
.Misc
.createDebounceHandler(500, (e
) => {
449 downloadLimitFN
= () => {
450 const hashes
= torrentsTable
.selectedRowsIds();
453 id
: "downloadLimitPage",
454 icon
: "images/qbittorrent-tray.svg",
455 title
: "QBT_TR(Torrent Download Speed Limiting)QBT_TR[CONTEXT=TransferListWidget]",
456 loadMethod
: "iframe",
457 contentURL
: new URI("downloadlimit.html").setData("hashes", hashes
.join("|")).toString(),
462 paddingHorizontal
: 0,
469 deleteSelectedTorrentsFN
= (forceDeleteFiles
= false) => {
470 const hashes
= torrentsTable
.selectedRowsIds();
471 if (hashes
.length
> 0) {
472 if (window
.qBittorrent
.Cache
.preferences
.get().confirm_torrent_deletion
) {
474 ...window
.qBittorrent
.Dialog
.baseModalOptions
,
475 id
: "confirmDeletionPage",
476 title
: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
479 forceDeleteFiles
: forceDeleteFiles
481 contentURL
: "views/confirmdeletion.html",
482 onContentLoaded
: (w
) => {
483 MochaUI
.resizeWindow(w
, { centered
: true });
484 MochaUI
.centerWindow(w
);
486 onCloseComplete
: () => {
487 // make sure overlay is properly hidden upon modal closing
488 document
.getElementById("modalOverlay").style
.display
= "none";
493 fetch("api/v2/torrents/delete", {
495 body
: new URLSearchParams({
496 hashes
: hashes
.join("|"),
497 deleteFiles
: forceDeleteFiles
500 .then((response
) => {
502 alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
506 torrentsTable
.deselectAll();
508 updatePropertiesPanel();
514 addClickEvent("delete", (e
) => {
517 deleteSelectedTorrentsFN();
521 const hashes
= torrentsTable
.selectedRowsIds();
523 fetch("api/v2/torrents/stop", {
525 body
: new URLSearchParams({
526 hashes
: hashes
.join("|")
534 const hashes
= torrentsTable
.selectedRowsIds();
536 fetch("api/v2/torrents/start", {
538 body
: new URLSearchParams({
539 hashes
: hashes
.join("|")
546 autoTorrentManagementFN
= () => {
547 const hashes
= torrentsTable
.selectedRowsIds();
548 if (hashes
.length
> 0) {
549 const enableAutoTMM
= hashes
.some((hash
) => !(torrentsTable
.getRow(hash
).full_data
.auto_tmm
));
552 ...window
.qBittorrent
.Dialog
.baseModalOptions
,
553 id
: "confirmAutoTMMDialog",
554 title
: "QBT_TR(Enable automatic torrent management)QBT_TR[CONTEXT=confirmAutoTMMDialog]",
557 enable
: enableAutoTMM
559 contentURL
: "views/confirmAutoTMM.html"
563 fetch("api/v2/torrents/setAutoManagement", {
565 body
: new URLSearchParams({
566 hashes
: hashes
.join("|"),
567 enable
: enableAutoTMM
570 .then((response
) => {
572 alert("QBT_TR(Unable to set Auto Torrent Management for the selected torrents.)QBT_TR[CONTEXT=HttpServer]");
583 const hashes
= torrentsTable
.selectedRowsIds();
584 if (hashes
.length
> 0) {
585 if (window
.qBittorrent
.Cache
.preferences
.get().confirm_torrent_recheck
) {
587 ...window
.qBittorrent
.Dialog
.baseModalOptions
,
588 id
: "confirmRecheckDialog",
589 title
: "QBT_TR(Recheck confirmation)QBT_TR[CONTEXT=confirmRecheckDialog]",
590 data
: { hashes
: hashes
},
591 contentURL
: "views/confirmRecheck.html"
595 fetch("api/v2/torrents/recheck", {
597 body
: new URLSearchParams({
598 hashes
: hashes
.join("|"),
601 .then((response
) => {
603 alert("QBT_TR(Unable to recheck torrents.)QBT_TR[CONTEXT=HttpServer]");
613 reannounceFN
= () => {
614 const hashes
= torrentsTable
.selectedRowsIds();
616 fetch("api/v2/torrents/reannounce", {
618 body
: new URLSearchParams({
619 hashes
: hashes
.join("|")
626 setLocationFN
= () => {
627 const hashes
= torrentsTable
.selectedRowsIds();
629 const hash
= hashes
[0];
630 const row
= torrentsTable
.getRow(hash
);
633 id
: "setLocationPage",
634 icon
: "images/qbittorrent-tray.svg",
635 title
: "QBT_TR(Set location)QBT_TR[CONTEXT=TransferListWidget]",
636 loadMethod
: "iframe",
637 contentURL
: new URI("setlocation.html").setData("hashes", hashes
.join("|")).setData("path", encodeURIComponent(row
.full_data
.save_path
)).toString(),
642 paddingHorizontal
: 0,
650 const hashes
= torrentsTable
.selectedRowsIds();
651 if (hashes
.length
=== 1) {
652 const hash
= hashes
[0];
653 const row
= torrentsTable
.getRow(hash
);
657 icon
: "images/qbittorrent-tray.svg",
658 title
: "QBT_TR(Rename)QBT_TR[CONTEXT=TransferListWidget]",
659 loadMethod
: "iframe",
660 contentURL
: new URI("rename.html").setData("hash", hash
).setData("name", row
.full_data
.name
).toString(),
665 paddingHorizontal
: 0,
673 renameFilesFN
= () => {
674 const hashes
= torrentsTable
.selectedRowsIds();
675 if (hashes
.length
=== 1) {
676 const hash
= hashes
[0];
677 const row
= torrentsTable
.getRow(hash
);
680 id
: "multiRenamePage",
681 icon
: "images/qbittorrent-tray.svg",
682 title
: "QBT_TR(Renaming)QBT_TR[CONTEXT=TransferListWidget]",
683 data
: { hash
: hash
, selectedRows
: [] },
685 contentURL
: "rename_files.html",
690 paddingHorizontal
: 0,
693 resizeLimit
: { x
: [800], y
: [420] }
699 startVisibleTorrentsFN
= () => {
700 const hashes
= torrentsTable
.getFilteredTorrentsHashes(selectedStatus
, selectedCategory
, selectedTag
, selectedTracker
);
701 if (hashes
.length
> 0) {
702 fetch("api/v2/torrents/start", {
704 body
: new URLSearchParams({
705 hashes
: hashes
.join("|")
708 .then((response
) => {
710 alert("QBT_TR(Unable to start torrents.)QBT_TR[CONTEXT=HttpServer]");
715 updatePropertiesPanel();
720 stopVisibleTorrentsFN
= () => {
721 const hashes
= torrentsTable
.getFilteredTorrentsHashes(selectedStatus
, selectedCategory
, selectedTag
, selectedTracker
);
722 if (hashes
.length
> 0) {
723 fetch("api/v2/torrents/stop", {
725 body
: new URLSearchParams({
726 hashes
: hashes
.join("|")
729 .then((response
) => {
731 alert("QBT_TR(Unable to stop torrents.)QBT_TR[CONTEXT=HttpServer]");
736 updatePropertiesPanel();
741 deleteVisibleTorrentsFN
= () => {
742 const hashes
= torrentsTable
.getFilteredTorrentsHashes(selectedStatus
, selectedCategory
, selectedTag
, selectedTracker
);
743 if (hashes
.length
> 0) {
744 if (window
.qBittorrent
.Cache
.preferences
.get().confirm_torrent_deletion
) {
746 ...window
.qBittorrent
.Dialog
.baseModalOptions
,
747 id
: "confirmDeletionPage",
748 title
: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
751 isDeletingVisibleTorrents
: true
753 contentURL
: "views/confirmdeletion.html",
754 onContentLoaded
: (w
) => {
755 MochaUI
.resizeWindow(w
, { centered
: true });
756 MochaUI
.centerWindow(w
);
761 fetch("api/v2/torrents/delete", {
763 body
: new URLSearchParams({
764 hashes
: hashes
.join("|"),
768 .then((response
) => {
770 alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
774 torrentsTable
.deselectAll();
776 updatePropertiesPanel();
782 torrentNewCategoryFN
= () => {
783 const action
= "set";
784 const hashes
= torrentsTable
.selectedRowsIds();
787 id
: "newCategoryPage",
788 icon
: "images/qbittorrent-tray.svg",
789 title
: "QBT_TR(New Category)QBT_TR[CONTEXT=TransferListWidget]",
790 loadMethod
: "iframe",
791 contentURL
: new URI("newcategory.html").setData("action", action
).setData("hashes", hashes
.join("|")).toString(),
796 paddingHorizontal
: 0,
803 torrentSetCategoryFN
= (categoryHash
) => {
804 const hashes
= torrentsTable
.selectedRowsIds();
805 if (hashes
.length
<= 0)
808 const categoryName
= category_list
.has(categoryHash
)
809 ? category_list
.get(categoryHash
).name
811 fetch("api/v2/torrents/setCategory", {
813 body
: new URLSearchParams({
814 hashes
: hashes
.join("|"),
815 category
: categoryName
818 .then((response
) => {
826 createCategoryFN
= () => {
827 const action
= "create";
829 id
: "newCategoryPage",
830 icon
: "images/qbittorrent-tray.svg",
831 title
: "QBT_TR(New Category)QBT_TR[CONTEXT=CategoryFilterWidget]",
832 loadMethod
: "iframe",
833 contentURL
: new URI("newcategory.html").setData("action", action
).toString(),
838 paddingHorizontal
: 0,
844 createSubcategoryFN
= (categoryHash
) => {
845 const action
= "createSubcategory";
846 const categoryName
= category_list
.get(categoryHash
).name
+ "/";
848 id
: "newSubcategoryPage",
849 icon
: "images/qbittorrent-tray.svg",
850 title
: "QBT_TR(New Category)QBT_TR[CONTEXT=CategoryFilterWidget]",
851 loadMethod
: "iframe",
852 contentURL
: new URI("newcategory.html").setData("action", action
).setData("categoryName", categoryName
).toString(),
857 paddingHorizontal
: 0,
863 editCategoryFN
= (categoryHash
) => {
864 const action
= "edit";
865 const category
= category_list
.get(categoryHash
);
867 id
: "editCategoryPage",
868 icon
: "images/qbittorrent-tray.svg",
869 title
: "QBT_TR(Edit Category)QBT_TR[CONTEXT=TransferListWidget]",
870 loadMethod
: "iframe",
871 contentURL
: new URI("newcategory.html").setData("action", action
).setData("categoryName", category
.name
).setData("savePath", category
.savePath
).toString(),
876 paddingHorizontal
: 0,
882 removeCategoryFN
= (categoryHash
) => {
883 fetch("api/v2/torrents/removeCategories", {
885 body
: new URLSearchParams({
886 categories
: category_list
.get(categoryHash
).name
889 .then((response
) => {
893 setCategoryFilter(CATEGORIES_ALL
);
898 deleteUnusedCategoriesFN
= () => {
899 const categories
= [];
900 category_list
.forEach((category
, hash
) => {
901 if (torrentsTable
.getFilteredTorrentsNumber("all", hash
, TAGS_ALL
, TRACKERS_ALL
) === 0)
902 categories
.push(category
.name
);
904 fetch("api/v2/torrents/removeCategories", {
906 body
: new URLSearchParams({
907 categories
: categories
.join("\n")
910 .then((response
) => {
914 setCategoryFilter(CATEGORIES_ALL
);
919 torrentAddTagsFN
= () => {
920 const action
= "set";
921 const hashes
= torrentsTable
.selectedRowsIds();
925 icon
: "images/qbittorrent-tray.svg",
926 title
: "QBT_TR(Add tags)QBT_TR[CONTEXT=TransferListWidget]",
927 loadMethod
: "iframe",
928 contentURL
: new URI("newtag.html").setData("action", action
).setData("hashes", hashes
.join("|")).toString(),
933 paddingHorizontal
: 0,
940 torrentSetTagsFN
= (tagHash
, isSet
) => {
941 const hashes
= torrentsTable
.selectedRowsIds();
942 if (hashes
.length
<= 0)
945 fetch((isSet
? "api/v2/torrents/addTags" : "api/v2/torrents/removeTags"), {
947 body
: new URLSearchParams({
948 hashes
: hashes
.join("|"),
949 tags
: (tagList
.get(tagHash
)?.name
|| "")
954 torrentRemoveAllTagsFN
= () => {
955 const hashes
= torrentsTable
.selectedRowsIds();
957 fetch("api/v2/torrents/removeTags", {
959 body
: new URLSearchParams({
960 hashes
: hashes
.join("|")
966 createTagFN
= () => {
967 const action
= "create";
970 icon
: "images/qbittorrent-tray.svg",
971 title
: "QBT_TR(New Tag)QBT_TR[CONTEXT=TagFilterWidget]",
972 loadMethod
: "iframe",
973 contentURL
: new URI("newtag.html").setData("action", action
).toString(),
978 paddingHorizontal
: 0,
985 removeTagFN
= (tagHash
) => {
986 fetch("api/v2/torrents/deleteTags", {
988 body
: new URLSearchParams({
989 tags
: tagList
.get(tagHash
).name
992 setTagFilter(TAGS_ALL
);
995 deleteUnusedTagsFN
= () => {
997 tagList
.forEach((tag
, hash
) => {
998 if (torrentsTable
.getFilteredTorrentsNumber("all", CATEGORIES_ALL
, hash
, TRACKERS_ALL
) === 0)
1001 fetch("api/v2/torrents/deleteTags", {
1003 body
: new URLSearchParams({
1004 tags
: tags
.join(",")
1007 setTagFilter(TAGS_ALL
);
1010 deleteTrackerFN
= (trackerHash
) => {
1011 const trackerHashInt
= Number(trackerHash
);
1012 if ((trackerHashInt
=== TRACKERS_ALL
) || (trackerHashInt
=== TRACKERS_TRACKERLESS
))
1015 const tracker
= trackerList
.get(trackerHashInt
);
1016 const host
= tracker
.host
;
1017 const urls
= [...tracker
.trackerTorrentMap
.keys()];
1019 new MochaUI
.Window({
1020 id
: "confirmDeletionPage",
1021 title
: "QBT_TR(Remove tracker)QBT_TR[CONTEXT=confirmDeletionDlg]",
1022 loadMethod
: "iframe",
1023 contentURL
: new URI("confirmtrackerdeletion.html").setData("host", host
).setData("urls", urls
.map(encodeURIComponent
).join("|")).toString(),
1030 onCloseComplete
: () => {
1032 setTrackerFilter(TRACKERS_ALL
);
1037 copyNameFN
= () => {
1038 const selectedRows
= torrentsTable
.selectedRowsIds();
1040 if (selectedRows
.length
> 0) {
1041 const rows
= torrentsTable
.getFilteredAndSortedRows();
1042 for (let i
= 0; i
< selectedRows
.length
; ++i
) {
1043 const hash
= selectedRows
[i
];
1044 names
.push(rows
[hash
].full_data
.name
);
1047 return names
.join("\n");
1050 copyInfohashFN
= (policy
) => {
1051 const selectedRows
= torrentsTable
.selectedRowsIds();
1052 const infohashes
= [];
1053 if (selectedRows
.length
> 0) {
1054 const rows
= torrentsTable
.getFilteredAndSortedRows();
1057 for (const id
of selectedRows
) {
1058 const infohash
= rows
[id
].full_data
.infohash_v1
;
1059 if (infohash
!== "")
1060 infohashes
.push(infohash
);
1064 for (const id
of selectedRows
) {
1065 const infohash
= rows
[id
].full_data
.infohash_v2
;
1066 if (infohash
!== "")
1067 infohashes
.push(infohash
);
1072 return infohashes
.join("\n");
1075 copyMagnetLinkFN
= () => {
1076 const selectedRows
= torrentsTable
.selectedRowsIds();
1078 if (selectedRows
.length
> 0) {
1079 const rows
= torrentsTable
.getFilteredAndSortedRows();
1080 for (let i
= 0; i
< selectedRows
.length
; ++i
) {
1081 const hash
= selectedRows
[i
];
1082 magnets
.push(rows
[hash
].full_data
.magnet_uri
);
1085 return magnets
.join("\n");
1089 return torrentsTable
.selectedRowsIds().join("\n");
1092 copyCommentFN
= () => {
1093 const selectedRows
= torrentsTable
.selectedRowsIds();
1094 const comments
= [];
1095 if (selectedRows
.length
> 0) {
1096 const rows
= torrentsTable
.getFilteredAndSortedRows();
1097 for (let i
= 0; i
< selectedRows
.length
; ++i
) {
1098 const hash
= selectedRows
[i
];
1099 const comment
= rows
[hash
].full_data
.comment
;
1100 if (comment
&& (comment
!== ""))
1101 comments
.push(comment
);
1104 return comments
.join("\n---------\n");
1107 exportTorrentFN
= async () => {
1108 const hashes
= torrentsTable
.selectedRowsIds();
1109 for (const hash
of hashes
) {
1110 const row
= torrentsTable
.getRow(hash
);
1114 const name
= row
.full_data
.name
;
1115 const url
= new URI("api/v2/torrents/export").setData("hash", hash
).toString();
1117 // download response to file
1118 await window
.qBittorrent
.Misc
.downloadFile(url
, `${name}.torrent`, "QBT_TR(Unable to export torrent file)QBT_TR[CONTEXT=MainWindow]");
1120 // https://stackoverflow.com/questions/53560991/automatic-file-downloads-limited-to-10-files-on-chrome-browser
1121 await window
.qBittorrent
.Misc
.sleep(200);
1125 addClickEvent("stopAll", (e
) => {
1127 e
.stopPropagation();
1129 if (confirm("QBT_TR(Would you like to stop all torrents?)QBT_TR[CONTEXT=MainWindow]")) {
1130 fetch("api/v2/torrents/stop", {
1132 body
: new URLSearchParams({
1140 addClickEvent("startAll", (e
) => {
1142 e
.stopPropagation();
1144 if (confirm("QBT_TR(Would you like to start all torrents?)QBT_TR[CONTEXT=MainWindow]")) {
1145 fetch("api/v2/torrents/start", {
1147 body
: new URLSearchParams({
1155 ["stop", "start", "recheck"].each((item
) => {
1156 addClickEvent(item
, (e
) => {
1158 e
.stopPropagation();
1160 const hashes
= torrentsTable
.selectedRowsIds();
1161 if (hashes
.length
) {
1162 hashes
.each((hash
, index
) => {
1163 fetch(`api/v2/torrents/${item}`, {
1165 body
: new URLSearchParams({
1175 ["decreasePrio", "increasePrio", "topPrio", "bottomPrio"].each((item
) => {
1176 addClickEvent(item
, (e
) => {
1178 e
.stopPropagation();
1179 setQueuePositionFN(item
);
1183 setQueuePositionFN
= (cmd
) => {
1184 const hashes
= torrentsTable
.selectedRowsIds();
1185 if (hashes
.length
) {
1186 fetch(`api/v2/torrents/${cmd}`, {
1188 body
: new URLSearchParams({
1189 hashes
: hashes
.join("|")
1196 addClickEvent("about", (e
) => {
1198 e
.stopPropagation();
1200 const id
= "aboutpage";
1201 new MochaUI
.Window({
1203 icon
: "images/qbittorrent-tray.svg",
1204 title
: "QBT_TR(About qBittorrent)QBT_TR[CONTEXT=AboutDialog]",
1206 contentURL
: new URI("views/about.html").toString(),
1208 css
: ["css/Tabs.css"]
1211 toolbarURL
: "views/aboutToolbar.html",
1213 width
: loadWindowWidth(id
, 570),
1214 height
: loadWindowHeight(id
, 360),
1215 onResize
: window
.qBittorrent
.Misc
.createDebounceHandler(500, (e
) => {
1221 addClickEvent("logout", (e
) => {
1223 e
.stopPropagation();
1225 fetch("api/v2/auth/logout", {
1228 .then((response
) => {
1232 window
.location
.reload(true);
1236 addClickEvent("shutdown", (e
) => {
1238 e
.stopPropagation();
1240 if (confirm("QBT_TR(Are you sure you want to quit qBittorrent?)QBT_TR[CONTEXT=MainWindow]")) {
1241 fetch("api/v2/app/shutdown", {
1244 .then((response
) => {
1248 const shutdownMessage
= "QBT_TR(%1 has been shutdown)QBT_TR[CONTEXT=HttpServer]".replace("%1", window
.qBittorrent
.Client
.mainTitle());
1249 document
.write(`<!doctype html><html lang="${LANG}"><head> <meta charset="UTF-8"> <meta name="color-scheme" content="light dark"> <title>${shutdownMessage}</title> <style>* {font-family: Arial, Helvetica, sans-serif;}</style></head><body> <h1 style="text-align: center;">${shutdownMessage}</h1></body></html>`);
1252 window
.qBittorrent
.Client
.stop();
1257 // Deactivate menu header links
1258 $$("a.returnFalse").each((el
) => {
1259 el
.addEventListener("click", (e
) => {
1261 e
.stopPropagation();