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
52 const keys = Reflect.ownKeys(obj);
53 for (const key of keys) {
54 const value = obj[key];
55 if ((value && (typeof value === "object")) || (typeof value === "function"))
61 const baseModalOptions = Object.assign(Object.create(null), {
62 addClass: "modalDialog",
67 icon: "images/qbittorrent-tray.svg",
82 deepFreeze(baseModalOptions);
86 Object.freeze(window.qBittorrent.Dialog);
88 const LocalPreferences = new window.qBittorrent.LocalPreferences.LocalPreferencesClass();
90 let saveWindowSize = function() {};
91 let loadWindowWidth = function() {};
92 let loadWindowHeight = function() {};
93 let showDownloadPage = function() {};
94 let globalUploadLimitFN = function() {};
95 let uploadLimitFN = function() {};
96 let shareRatioFN = function() {};
97 let toggleSequentialDownloadFN = function() {};
98 let toggleFirstLastPiecePrioFN = function() {};
99 let setSuperSeedingFN = function() {};
100 let setForceStartFN = function() {};
101 let globalDownloadLimitFN = function() {};
102 let StatisticsLinkFN = function() {};
103 let downloadLimitFN = function() {};
104 let deleteFN = function() {};
105 let stopFN = function() {};
106 let startFN = function() {};
107 let autoTorrentManagementFN = function() {};
108 let recheckFN = function() {};
109 let reannounceFN = function() {};
110 let setLocationFN = function() {};
111 let renameFN = function() {};
112 let renameFilesFN = function() {};
113 let torrentNewCategoryFN = function() {};
114 let torrentSetCategoryFN = function() {};
115 let createCategoryFN = function() {};
116 let createSubcategoryFN = function() {};
117 let editCategoryFN = function() {};
118 let removeCategoryFN = function() {};
119 let deleteUnusedCategoriesFN = function() {};
120 let startTorrentsByCategoryFN = function() {};
121 let stopTorrentsByCategoryFN = function() {};
122 let deleteTorrentsByCategoryFN = function() {};
123 let torrentAddTagsFN = function() {};
124 let torrentSetTagsFN = function() {};
125 let torrentRemoveAllTagsFN = function() {};
126 let createTagFN = function() {};
127 let removeTagFN = function() {};
128 let deleteUnusedTagsFN = function() {};
129 let startTorrentsByTagFN = function() {};
130 let stopTorrentsByTagFN = function() {};
131 let deleteTorrentsByTagFN = function() {};
132 let startTorrentsByTrackerFN = function() {};
133 let stopTorrentsByTrackerFN = function() {};
134 let deleteTorrentsByTrackerFN = function() {};
135 let copyNameFN = function() {};
136 let copyInfohashFN = function(policy) {};
137 let copyMagnetLinkFN = function() {};
138 let copyIdFN = function() {};
139 let copyCommentFN = function() {};
140 let setQueuePositionFN = function() {};
141 let exportTorrentFN = function() {};
143 const initializeWindows = function() {
144 saveWindowSize = function(windowId) {
145 const size = $(windowId).getSize();
146 LocalPreferences.set("window_" + windowId + "_width", size.x);
147 LocalPreferences.set("window_" + windowId + "_height", size.y);
150 loadWindowWidth = function(windowId, defaultValue) {
151 return LocalPreferences.get("window_" + windowId + "_width", defaultValue);
154 loadWindowHeight = function(windowId, defaultValue) {
155 return LocalPreferences.get("window_" + windowId + "_height", defaultValue);
158 function addClickEvent(el, fn) {
159 ["Link", "Button"].each((item) => {
161 $(el + item).addEventListener("click", fn);
165 addClickEvent("download", (e) => {
171 showDownloadPage = function(urls) {
172 const id = "downloadPage";
173 const contentUri = new URI("download.html");
175 if (urls && (urls.length > 0))
176 contentUri.setData("urls", urls.map(encodeURIComponent).join("|"));
180 icon: "images/qbittorrent-tray.svg",
181 title: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
182 loadMethod: "iframe",
183 contentURL: contentUri.toString(),
184 addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
189 paddingHorizontal: 0,
190 width: loadWindowWidth(id, 500),
191 height: loadWindowHeight(id, 600),
192 onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
199 addClickEvent("preferences", (e) => {
203 const id = "preferencesPage";
206 icon: "images/qbittorrent-tray.svg",
207 title: "QBT_TR(Options)QBT_TR[CONTEXT=OptionsDialog]",
210 contentURL: new URI("views/preferences.html").toString(),
212 css: ["css/Tabs.css"]
214 toolbarURL: "views/preferencesToolbar.html",
218 paddingHorizontal: 0,
219 width: loadWindowWidth(id, 700),
220 height: loadWindowHeight(id, 600),
221 onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
227 addClickEvent("upload", (e) => {
231 const id = "uploadPage";
234 icon: "images/qbittorrent-tray.svg",
235 title: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
236 loadMethod: "iframe",
237 contentURL: new URI("upload.html").toString(),
238 addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
242 paddingHorizontal: 0,
243 width: loadWindowWidth(id, 500),
244 height: loadWindowHeight(id, 460),
245 onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
252 globalUploadLimitFN = function() {
254 id: "uploadLimitPage",
255 icon: "images/qbittorrent-tray.svg",
256 title: "QBT_TR(Global Upload Speed Limit)QBT_TR[CONTEXT=MainWindow]",
257 loadMethod: "iframe",
258 contentURL: new URI("uploadlimit.html").setData("hashes", "global").toString(),
263 paddingHorizontal: 0,
269 uploadLimitFN = function() {
270 const hashes = torrentsTable.selectedRowsIds();
273 id: "uploadLimitPage",
274 icon: "images/qbittorrent-tray.svg",
275 title: "QBT_TR(Torrent Upload Speed Limiting)QBT_TR[CONTEXT=TransferListWidget]",
276 loadMethod: "iframe",
277 contentURL: new URI("uploadlimit.html").setData("hashes", hashes.join("|")).toString(),
282 paddingHorizontal: 0,
289 shareRatioFN = function() {
290 const hashes = torrentsTable.selectedRowsIds();
292 let shareRatio = null;
293 let torrentsHaveSameShareRatio = true;
295 // check if all selected torrents have same share ratio
296 for (let i = 0; i < hashes.length; ++i) {
297 const hash = hashes[i];
298 const row = torrentsTable.rows[hash].full_data;
299 const origValues = row.ratio_limit + "|" + row.seeding_time_limit + "|" + row.inactive_seeding_time_limit + "|"
300 + row.max_ratio + "|" + row.max_seeding_time + "|" + row.max_inactive_seeding_time;
303 if (shareRatio === null)
304 shareRatio = origValues;
306 if (origValues !== shareRatio) {
307 torrentsHaveSameShareRatio = false;
312 // if all torrents have same share ratio, display that share ratio. else use the default
313 const orig = torrentsHaveSameShareRatio ? shareRatio : "";
315 id: "shareRatioPage",
316 icon: "images/qbittorrent-tray.svg",
317 title: "QBT_TR(Torrent Upload/Download Ratio Limiting)QBT_TR[CONTEXT=UpDownRatioDialog]",
318 loadMethod: "iframe",
319 contentURL: new URI("shareratio.html").setData("hashes", hashes.join("|")).setData("orig", orig).toString(),
323 paddingHorizontal: 0,
330 toggleSequentialDownloadFN = function() {
331 const hashes = torrentsTable.selectedRowsIds();
334 url: "api/v2/torrents/toggleSequentialDownload",
337 hashes: hashes.join("|")
344 toggleFirstLastPiecePrioFN = function() {
345 const hashes = torrentsTable.selectedRowsIds();
348 url: "api/v2/torrents/toggleFirstLastPiecePrio",
351 hashes: hashes.join("|")
358 setSuperSeedingFN = function(val) {
359 const hashes = torrentsTable.selectedRowsIds();
362 url: "api/v2/torrents/setSuperSeeding",
366 hashes: hashes.join("|")
373 setForceStartFN = function() {
374 const hashes = torrentsTable.selectedRowsIds();
377 url: "api/v2/torrents/setForceStart",
381 hashes: hashes.join("|")
388 globalDownloadLimitFN = function() {
390 id: "downloadLimitPage",
391 icon: "images/qbittorrent-tray.svg",
392 title: "QBT_TR(Global Download Speed Limit)QBT_TR[CONTEXT=MainWindow]",
393 loadMethod: "iframe",
394 contentURL: new URI("downloadlimit.html").setData("hashes", "global").toString(),
399 paddingHorizontal: 0,
405 StatisticsLinkFN = function() {
406 const id = "statisticspage";
409 icon: "images/qbittorrent-tray.svg",
410 title: "QBT_TR(Statistics)QBT_TR[CONTEXT=StatsDialog]",
412 contentURL: new URI("views/statistics.html").toString(),
415 width: loadWindowWidth(id, 275),
416 height: loadWindowHeight(id, 370),
417 onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
423 downloadLimitFN = function() {
424 const hashes = torrentsTable.selectedRowsIds();
427 id: "downloadLimitPage",
428 icon: "images/qbittorrent-tray.svg",
429 title: "QBT_TR(Torrent Download Speed Limiting)QBT_TR[CONTEXT=TransferListWidget]",
430 loadMethod: "iframe",
431 contentURL: new URI("downloadlimit.html").setData("hashes", hashes.join("|")).toString(),
436 paddingHorizontal: 0,
443 deleteFN = function(forceDeleteFiles = false) {
444 const hashes = torrentsTable.selectedRowsIds();
445 if (hashes.length > 0) {
446 if (window.qBittorrent.Cache.preferences.get().confirm_torrent_deletion) {
448 ...window.qBittorrent.Dialog.baseModalOptions,
449 id: "confirmDeletionPage",
450 title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
453 forceDeleteFiles: forceDeleteFiles
455 contentURL: "views/confirmdeletion.html",
456 onContentLoaded: function(w) {
457 MochaUI.resizeWindow(w, { centered: true });
458 MochaUI.centerWindow(w);
460 onCloseComplete: function() {
461 // make sure overlay is properly hidden upon modal closing
462 document.getElementById("modalOverlay").style.display = "none";
468 url: "api/v2/torrents/delete",
471 hashes: hashes.join("|"),
472 deleteFiles: forceDeleteFiles
474 onSuccess: function() {
475 torrentsTable.deselectAll();
478 onFailure: function() {
479 alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
486 addClickEvent("delete", (e) => {
492 stopFN = function() {
493 const hashes = torrentsTable.selectedRowsIds();
496 url: "api/v2/torrents/stop",
499 hashes: hashes.join("|")
506 startFN = function() {
507 const hashes = torrentsTable.selectedRowsIds();
510 url: "api/v2/torrents/start",
513 hashes: hashes.join("|")
520 autoTorrentManagementFN = function() {
521 const hashes = torrentsTable.selectedRowsIds();
524 hashes.each((hash, index) => {
525 const row = torrentsTable.rows[hash];
526 if (!row.full_data.auto_tmm)
530 url: "api/v2/torrents/setAutoManagement",
533 hashes: hashes.join("|"),
541 recheckFN = function() {
542 const hashes = torrentsTable.selectedRowsIds();
545 url: "api/v2/torrents/recheck",
548 hashes: hashes.join("|"),
555 reannounceFN = function() {
556 const hashes = torrentsTable.selectedRowsIds();
559 url: "api/v2/torrents/reannounce",
562 hashes: hashes.join("|"),
569 setLocationFN = function() {
570 const hashes = torrentsTable.selectedRowsIds();
572 const hash = hashes[0];
573 const row = torrentsTable.rows[hash];
576 id: "setLocationPage",
577 icon: "images/qbittorrent-tray.svg",
578 title: "QBT_TR(Set location)QBT_TR[CONTEXT=TransferListWidget]",
579 loadMethod: "iframe",
580 contentURL: new URI("setlocation.html").setData("hashes", hashes.join("|")).setData("path", encodeURIComponent(row.full_data.save_path)).toString(),
585 paddingHorizontal: 0,
592 renameFN = function() {
593 const hashes = torrentsTable.selectedRowsIds();
594 if (hashes.length === 1) {
595 const hash = hashes[0];
596 const row = torrentsTable.rows[hash];
600 icon: "images/qbittorrent-tray.svg",
601 title: "QBT_TR(Rename)QBT_TR[CONTEXT=TransferListWidget]",
602 loadMethod: "iframe",
603 contentURL: new URI("rename.html").setData("hash", hash).setData("name", row.full_data.name).toString(),
608 paddingHorizontal: 0,
616 renameFilesFN = function() {
617 const hashes = torrentsTable.selectedRowsIds();
618 if (hashes.length === 1) {
619 const hash = hashes[0];
620 const row = torrentsTable.rows[hash];
623 id: "multiRenamePage",
624 icon: "images/qbittorrent-tray.svg",
625 title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TransferListWidget]",
626 data: { hash: hash, selectedRows: [] },
628 contentURL: "rename_files.html",
633 paddingHorizontal: 0,
636 resizeLimit: { "x": [800], "y": [420] }
642 torrentNewCategoryFN = function() {
643 const action = "set";
644 const hashes = torrentsTable.selectedRowsIds();
647 id: "newCategoryPage",
648 icon: "images/qbittorrent-tray.svg",
649 title: "QBT_TR(New Category)QBT_TR[CONTEXT=TransferListWidget]",
650 loadMethod: "iframe",
651 contentURL: new URI("newcategory.html").setData("action", action).setData("hashes", hashes.join("|")).toString(),
656 paddingHorizontal: 0,
663 torrentSetCategoryFN = function(categoryHash) {
664 const hashes = torrentsTable.selectedRowsIds();
665 if (hashes.length <= 0)
668 const categoryName = category_list.has(categoryHash)
669 ? category_list.get(categoryHash).name
672 url: "api/v2/torrents/setCategory",
675 hashes: hashes.join("|"),
676 category: categoryName
678 onSuccess: function() {
684 createCategoryFN = function() {
685 const action = "create";
687 id: "newCategoryPage",
688 icon: "images/qbittorrent-tray.svg",
689 title: "QBT_TR(New Category)QBT_TR[CONTEXT=CategoryFilterWidget]",
690 loadMethod: "iframe",
691 contentURL: new URI("newcategory.html").setData("action", action).toString(),
696 paddingHorizontal: 0,
702 createSubcategoryFN = function(categoryHash) {
703 const action = "createSubcategory";
704 const categoryName = category_list.get(categoryHash).name + "/";
706 id: "newSubcategoryPage",
707 icon: "images/qbittorrent-tray.svg",
708 title: "QBT_TR(New Category)QBT_TR[CONTEXT=CategoryFilterWidget]",
709 loadMethod: "iframe",
710 contentURL: new URI("newcategory.html").setData("action", action).setData("categoryName", categoryName).toString(),
715 paddingHorizontal: 0,
721 editCategoryFN = function(categoryHash) {
722 const action = "edit";
723 const category = category_list.get(categoryHash);
725 id: "editCategoryPage",
726 icon: "images/qbittorrent-tray.svg",
727 title: "QBT_TR(Edit Category)QBT_TR[CONTEXT=TransferListWidget]",
728 loadMethod: "iframe",
729 contentURL: new URI("newcategory.html").setData("action", action).setData("categoryName", category.name).setData("savePath", category.savePath).toString(),
734 paddingHorizontal: 0,
740 removeCategoryFN = function(categoryHash) {
741 const categoryName = category_list.get(categoryHash).name;
743 url: "api/v2/torrents/removeCategories",
746 categories: categoryName
748 onSuccess: function() {
749 setCategoryFilter(CATEGORIES_ALL);
755 deleteUnusedCategoriesFN = function() {
756 const categories = [];
757 category_list.forEach((category, hash) => {
758 if (torrentsTable.getFilteredTorrentsNumber("all", hash, TAGS_ALL, TRACKERS_ALL) === 0)
759 categories.push(category.name);
763 url: "api/v2/torrents/removeCategories",
766 categories: categories.join("\n")
768 onSuccess: function() {
769 setCategoryFilter(CATEGORIES_ALL);
775 startTorrentsByCategoryFN = function(categoryHash) {
776 const hashes = torrentsTable.getFilteredTorrentsHashes("all", categoryHash, TAGS_ALL, TRACKERS_ALL);
779 url: "api/v2/torrents/start",
782 hashes: hashes.join("|")
789 stopTorrentsByCategoryFN = function(categoryHash) {
790 const hashes = torrentsTable.getFilteredTorrentsHashes("all", categoryHash, TAGS_ALL, TRACKERS_ALL);
793 url: "api/v2/torrents/stop",
796 hashes: hashes.join("|")
803 deleteTorrentsByCategoryFN = function(categoryHash) {
804 const hashes = torrentsTable.getFilteredTorrentsHashes("all", categoryHash, TAGS_ALL, TRACKERS_ALL);
805 if (hashes.length > 0) {
806 if (window.qBittorrent.Cache.preferences.get().confirm_torrent_deletion) {
808 ...window.qBittorrent.Dialog.baseModalOptions,
809 id: "confirmDeletionPage",
810 title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
811 data: { hashes: hashes },
812 contentURL: "views/confirmdeletion.html",
813 onContentLoaded: function(w) {
814 MochaUI.resizeWindow(w, { centered: true });
815 MochaUI.centerWindow(w);
817 onCloseComplete: function() {
818 // make sure overlay is properly hidden upon modal closing
819 document.getElementById("modalOverlay").style.display = "none";
825 url: "api/v2/torrents/delete",
828 hashes: hashes.join("|"),
831 onSuccess: function() {
832 torrentsTable.deselectAll();
835 onFailure: function() {
836 alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
843 torrentAddTagsFN = function() {
844 const action = "set";
845 const hashes = torrentsTable.selectedRowsIds();
849 icon: "images/qbittorrent-tray.svg",
850 title: "QBT_TR(Add tags)QBT_TR[CONTEXT=TransferListWidget]",
851 loadMethod: "iframe",
852 contentURL: new URI("newtag.html").setData("action", action).setData("hashes", hashes.join("|")).toString(),
857 paddingHorizontal: 0,
864 torrentSetTagsFN = function(tagHash, isSet) {
865 const hashes = torrentsTable.selectedRowsIds();
866 if (hashes.length <= 0)
869 const tagName = tagList.has(tagHash) ? tagList.get(tagHash).name : "";
871 url: (isSet ? "api/v2/torrents/addTags" : "api/v2/torrents/removeTags"),
874 hashes: hashes.join("|"),
880 torrentRemoveAllTagsFN = function() {
881 const hashes = torrentsTable.selectedRowsIds();
884 url: ("api/v2/torrents/removeTags"),
887 hashes: hashes.join("|"),
893 createTagFN = function() {
894 const action = "create";
897 icon: "images/qbittorrent-tray.svg",
898 title: "QBT_TR(New Tag)QBT_TR[CONTEXT=TagFilterWidget]",
899 loadMethod: "iframe",
900 contentURL: new URI("newtag.html").setData("action", action).toString(),
905 paddingHorizontal: 0,
912 removeTagFN = function(tagHash) {
913 const tagName = tagList.get(tagHash).name;
915 url: "api/v2/torrents/deleteTags",
921 setTagFilter(TAGS_ALL);
924 deleteUnusedTagsFN = function() {
926 tagList.forEach((tag, hash) => {
927 if (torrentsTable.getFilteredTorrentsNumber("all", CATEGORIES_ALL, hash, TRACKERS_ALL) === 0)
931 url: "api/v2/torrents/deleteTags",
937 setTagFilter(TAGS_ALL);
940 startTorrentsByTagFN = function(tagHash) {
941 const hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, tagHash, TRACKERS_ALL);
944 url: "api/v2/torrents/start",
947 hashes: hashes.join("|")
954 stopTorrentsByTagFN = function(tagHash) {
955 const hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, tagHash, TRACKERS_ALL);
958 url: "api/v2/torrents/stop",
961 hashes: hashes.join("|")
968 deleteTorrentsByTagFN = function(tagHash) {
969 const hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, tagHash, TRACKERS_ALL);
970 if (hashes.length > 0) {
971 if (window.qBittorrent.Cache.preferences.get().confirm_torrent_deletion) {
973 ...window.qBittorrent.Dialog.baseModalOptions,
974 id: "confirmDeletionPage",
975 title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
976 data: { hashes: hashes },
977 contentURL: "views/confirmdeletion.html",
978 onContentLoaded: function(w) {
979 MochaUI.resizeWindow(w, { centered: true });
980 MochaUI.centerWindow(w);
982 onCloseComplete: function() {
983 // make sure overlay is properly hidden upon modal closing
984 document.getElementById("modalOverlay").style.display = "none";
990 url: "api/v2/torrents/delete",
993 hashes: hashes.join("|"),
996 onSuccess: function() {
997 torrentsTable.deselectAll();
1000 onFailure: function() {
1001 alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
1008 startTorrentsByTrackerFN = function(trackerHash) {
1009 const trackerHashInt = Number.parseInt(trackerHash, 10);
1011 switch (trackerHashInt) {
1013 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
1015 case TRACKERS_TRACKERLESS:
1016 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
1019 const uniqueTorrents = new Set();
1020 for (const torrents of trackerList.get(trackerHashInt).trackerTorrentMap.values()) {
1021 for (const torrent of torrents)
1022 uniqueTorrents.add(torrent);
1024 hashes = [...uniqueTorrents];
1029 if (hashes.length > 0) {
1031 url: "api/v2/torrents/start",
1034 hashes: hashes.join("|")
1041 stopTorrentsByTrackerFN = function(trackerHash) {
1042 const trackerHashInt = Number.parseInt(trackerHash, 10);
1044 switch (trackerHashInt) {
1046 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
1048 case TRACKERS_TRACKERLESS:
1049 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
1052 const uniqueTorrents = new Set();
1053 for (const torrents of trackerList.get(trackerHashInt).trackerTorrentMap.values()) {
1054 for (const torrent of torrents)
1055 uniqueTorrents.add(torrent);
1057 hashes = [...uniqueTorrents];
1062 if (hashes.length) {
1064 url: "api/v2/torrents/stop",
1067 hashes: hashes.join("|")
1074 deleteTorrentsByTrackerFN = function(trackerHash) {
1075 const trackerHashInt = Number.parseInt(trackerHash, 10);
1077 switch (trackerHashInt) {
1079 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
1081 case TRACKERS_TRACKERLESS:
1082 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
1085 const uniqueTorrents = new Set();
1086 for (const torrents of trackerList.get(trackerHashInt).trackerTorrentMap.values()) {
1087 for (const torrent of torrents)
1088 uniqueTorrents.add(torrent);
1090 hashes = [...uniqueTorrents];
1095 if (hashes.length > 0) {
1096 if (window.qBittorrent.Cache.preferences.get().confirm_torrent_deletion) {
1098 ...window.qBittorrent.Dialog.baseModalOptions,
1099 id: "confirmDeletionPage",
1100 title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
1103 filterList: "tracker"
1105 contentURL: "views/confirmdeletion.html",
1106 onContentLoaded: function(w) {
1107 MochaUI.resizeWindow(w, { centered: true });
1108 MochaUI.centerWindow(w);
1110 onCloseComplete: function() {
1111 // make sure overlay is properly hidden upon modal closing
1112 document.getElementById("modalOverlay").style.display = "none";
1118 url: "api/v2/torrents/delete",
1121 hashes: hashes.join("|"),
1124 onSuccess: function() {
1125 torrentsTable.deselectAll();
1126 setTrackerFilter(TRACKERS_ALL);
1129 onFailure: function() {
1130 alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
1137 copyNameFN = function() {
1138 const selectedRows = torrentsTable.selectedRowsIds();
1140 if (selectedRows.length > 0) {
1141 const rows = torrentsTable.getFilteredAndSortedRows();
1142 for (let i = 0; i < selectedRows.length; ++i) {
1143 const hash = selectedRows[i];
1144 names.push(rows[hash].full_data.name);
1147 return names.join("\n");
1150 copyInfohashFN = function(policy) {
1151 const selectedRows = torrentsTable.selectedRowsIds();
1152 const infohashes = [];
1153 if (selectedRows.length > 0) {
1154 const rows = torrentsTable.getFilteredAndSortedRows();
1157 for (const id of selectedRows) {
1158 const infohash = rows[id].full_data.infohash_v1;
1159 if (infohash !== "")
1160 infohashes.push(infohash);
1164 for (const id of selectedRows) {
1165 const infohash = rows[id].full_data.infohash_v2;
1166 if (infohash !== "")
1167 infohashes.push(infohash);
1172 return infohashes.join("\n");
1175 copyMagnetLinkFN = function() {
1176 const selectedRows = torrentsTable.selectedRowsIds();
1178 if (selectedRows.length > 0) {
1179 const rows = torrentsTable.getFilteredAndSortedRows();
1180 for (let i = 0; i < selectedRows.length; ++i) {
1181 const hash = selectedRows[i];
1182 magnets.push(rows[hash].full_data.magnet_uri);
1185 return magnets.join("\n");
1188 copyIdFN = function() {
1189 return torrentsTable.selectedRowsIds().join("\n");
1192 copyCommentFN = function() {
1193 const selectedRows = torrentsTable.selectedRowsIds();
1194 const comments = [];
1195 if (selectedRows.length > 0) {
1196 const rows = torrentsTable.getFilteredAndSortedRows();
1197 for (let i = 0; i < selectedRows.length; ++i) {
1198 const hash = selectedRows[i];
1199 const comment = rows[hash].full_data.comment;
1200 if (comment && (comment !== ""))
1201 comments.push(comment);
1204 return comments.join("\n---------\n");
1207 exportTorrentFN = async function() {
1208 const hashes = torrentsTable.selectedRowsIds();
1209 for (const hash of hashes) {
1210 const row = torrentsTable.rows.get(hash);
1214 const name = row.full_data.name;
1215 const url = new URI("api/v2/torrents/export");
1216 url.setData("hash", hash);
1218 // download response to file
1219 const element = document.createElement("a");
1221 element.download = (name + ".torrent");
1222 document.body.appendChild(element);
1224 document.body.removeChild(element);
1226 // https://stackoverflow.com/questions/53560991/automatic-file-downloads-limited-to-10-files-on-chrome-browser
1227 await window.qBittorrent.Misc.sleep(200);
1231 addClickEvent("stopAll", (e) => {
1233 e.stopPropagation();
1235 if (confirm("QBT_TR(Would you like to stop all torrents?)QBT_TR[CONTEXT=MainWindow]")) {
1237 url: "api/v2/torrents/stop",
1247 addClickEvent("startAll", (e) => {
1249 e.stopPropagation();
1251 if (confirm("QBT_TR(Would you like to start all torrents?)QBT_TR[CONTEXT=MainWindow]")) {
1253 url: "api/v2/torrents/start",
1263 ["stop", "start", "recheck"].each((item) => {
1264 addClickEvent(item, (e) => {
1266 e.stopPropagation();
1268 const hashes = torrentsTable.selectedRowsIds();
1269 if (hashes.length) {
1270 hashes.each((hash, index) => {
1272 url: "api/v2/torrents/" + item,
1284 ["decreasePrio", "increasePrio", "topPrio", "bottomPrio"].each((item) => {
1285 addClickEvent(item, (e) => {
1287 e.stopPropagation();
1288 setQueuePositionFN(item);
1292 setQueuePositionFN = function(cmd) {
1293 const hashes = torrentsTable.selectedRowsIds();
1294 if (hashes.length) {
1296 url: "api/v2/torrents/" + cmd,
1299 hashes: hashes.join("|")
1306 addClickEvent("about", (e) => {
1308 e.stopPropagation();
1310 const id = "aboutpage";
1311 new MochaUI.Window({
1313 icon: "images/qbittorrent-tray.svg",
1314 title: "QBT_TR(About qBittorrent)QBT_TR[CONTEXT=AboutDialog]",
1316 contentURL: new URI("views/about.html").toString(),
1318 css: ["css/Tabs.css"]
1321 toolbarURL: "views/aboutToolbar.html",
1323 width: loadWindowWidth(id, 550),
1324 height: loadWindowHeight(id, 360),
1325 onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
1331 addClickEvent("logout", (e) => {
1333 e.stopPropagation();
1336 url: "api/v2/auth/logout",
1338 onSuccess: function() {
1339 window.location.reload(true);
1344 addClickEvent("shutdown", (e) => {
1346 e.stopPropagation();
1348 if (confirm("QBT_TR(Are you sure you want to quit qBittorrent?)QBT_TR[CONTEXT=MainWindow]")) {
1350 url: "api/v2/app/shutdown",
1352 onSuccess: function() {
1353 const shutdownMessage = "QBT_TR(%1 has been shutdown)QBT_TR[CONTEXT=HttpServer]".replace("%1", window.qBittorrent.Client.mainTitle());
1354 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>`);
1357 window.qBittorrent.Client.stop();
1363 // Deactivate menu header links
1364 $$("a.returnFalse").each((el) => {
1365 el.addEventListener("click", (e) => {
1367 e.stopPropagation();