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: function() {
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.LocalPreferencesClass();
104 let saveWindowSize = function() {};
105 let loadWindowWidth = function() {};
106 let loadWindowHeight = function() {};
107 let showDownloadPage = function() {};
108 let globalUploadLimitFN = function() {};
109 let uploadLimitFN = function() {};
110 let shareRatioFN = function() {};
111 let toggleSequentialDownloadFN = function() {};
112 let toggleFirstLastPiecePrioFN = function() {};
113 let setSuperSeedingFN = function() {};
114 let setForceStartFN = function() {};
115 let globalDownloadLimitFN = function() {};
116 let StatisticsLinkFN = function() {};
117 let downloadLimitFN = function() {};
118 let deleteFN = function() {};
119 let stopFN = function() {};
120 let startFN = function() {};
121 let autoTorrentManagementFN = function() {};
122 let recheckFN = function() {};
123 let reannounceFN = function() {};
124 let setLocationFN = function() {};
125 let renameFN = function() {};
126 let renameFilesFN = function() {};
127 let torrentNewCategoryFN = function() {};
128 let torrentSetCategoryFN = function() {};
129 let createCategoryFN = function() {};
130 let createSubcategoryFN = function() {};
131 let editCategoryFN = function() {};
132 let removeCategoryFN = function() {};
133 let deleteUnusedCategoriesFN = function() {};
134 let startTorrentsByCategoryFN = function() {};
135 let stopTorrentsByCategoryFN = function() {};
136 let deleteTorrentsByCategoryFN = function() {};
137 let torrentAddTagsFN = function() {};
138 let torrentSetTagsFN = function() {};
139 let torrentRemoveAllTagsFN = function() {};
140 let createTagFN = function() {};
141 let removeTagFN = function() {};
142 let deleteUnusedTagsFN = function() {};
143 let startTorrentsByTagFN = function() {};
144 let stopTorrentsByTagFN = function() {};
145 let deleteTorrentsByTagFN = function() {};
146 let startTorrentsByTrackerFN = function() {};
147 let stopTorrentsByTrackerFN = function() {};
148 let deleteTorrentsByTrackerFN = function() {};
149 let deleteTrackerFN = function() {};
150 let copyNameFN = function() {};
151 let copyInfohashFN = function(policy) {};
152 let copyMagnetLinkFN = function() {};
153 let copyIdFN = function() {};
154 let copyCommentFN = function() {};
155 let setQueuePositionFN = function() {};
156 let exportTorrentFN = function() {};
158 const initializeWindows = function() {
159 saveWindowSize = function(windowId) {
160 const size = $(windowId).getSize();
161 LocalPreferences.set("window_" + windowId + "_width", size.x);
162 LocalPreferences.set("window_" + windowId + "_height", size.y);
165 loadWindowWidth = function(windowId, defaultValue) {
166 return LocalPreferences.get("window_" + windowId + "_width", defaultValue);
169 loadWindowHeight = function(windowId, defaultValue) {
170 return LocalPreferences.get("window_" + windowId + "_height", defaultValue);
173 function addClickEvent(el, fn) {
174 ["Link", "Button"].each((item) => {
176 $(el + item).addEventListener("click", fn);
180 addClickEvent("download", (e) => {
186 showDownloadPage = function(urls) {
187 const id = "downloadPage";
188 const contentUri = new URI("download.html");
190 if (urls && (urls.length > 0))
191 contentUri.setData("urls", urls.map(encodeURIComponent).join("|"));
195 icon: "images/qbittorrent-tray.svg",
196 title: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
197 loadMethod: "iframe",
198 contentURL: contentUri.toString(),
199 addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
204 paddingHorizontal: 0,
205 width: loadWindowWidth(id, 500),
206 height: loadWindowHeight(id, 600),
207 onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
214 addClickEvent("preferences", (e) => {
218 const id = "preferencesPage";
221 icon: "images/qbittorrent-tray.svg",
222 title: "QBT_TR(Options)QBT_TR[CONTEXT=OptionsDialog]",
225 contentURL: new URI("views/preferences.html").toString(),
227 css: ["css/Tabs.css"]
229 toolbarURL: "views/preferencesToolbar.html",
233 paddingHorizontal: 0,
234 width: loadWindowWidth(id, 700),
235 height: loadWindowHeight(id, 600),
236 onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
242 addClickEvent("upload", (e) => {
246 const id = "uploadPage";
249 icon: "images/qbittorrent-tray.svg",
250 title: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
251 loadMethod: "iframe",
252 contentURL: new URI("upload.html").toString(),
253 addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
257 paddingHorizontal: 0,
258 width: loadWindowWidth(id, 500),
259 height: loadWindowHeight(id, 460),
260 onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
267 globalUploadLimitFN = function() {
269 id: "uploadLimitPage",
270 icon: "images/qbittorrent-tray.svg",
271 title: "QBT_TR(Global Upload Speed Limit)QBT_TR[CONTEXT=MainWindow]",
272 loadMethod: "iframe",
273 contentURL: new URI("uploadlimit.html").setData("hashes", "global").toString(),
278 paddingHorizontal: 0,
284 uploadLimitFN = function() {
285 const hashes = torrentsTable.selectedRowsIds();
288 id: "uploadLimitPage",
289 icon: "images/qbittorrent-tray.svg",
290 title: "QBT_TR(Torrent Upload Speed Limiting)QBT_TR[CONTEXT=TransferListWidget]",
291 loadMethod: "iframe",
292 contentURL: new URI("uploadlimit.html").setData("hashes", hashes.join("|")).toString(),
297 paddingHorizontal: 0,
304 shareRatioFN = function() {
305 const hashes = torrentsTable.selectedRowsIds();
307 let shareRatio = null;
308 let torrentsHaveSameShareRatio = true;
310 // check if all selected torrents have same share ratio
311 for (let i = 0; i < hashes.length; ++i) {
312 const hash = hashes[i];
313 const row = torrentsTable.getRow(hash).full_data;
314 const origValues = row.ratio_limit + "|" + row.seeding_time_limit + "|" + row.inactive_seeding_time_limit + "|"
315 + row.max_ratio + "|" + row.max_seeding_time + "|" + row.max_inactive_seeding_time;
318 if (shareRatio === null)
319 shareRatio = origValues;
321 if (origValues !== shareRatio) {
322 torrentsHaveSameShareRatio = false;
327 // if all torrents have same share ratio, display that share ratio. else use the default
328 const orig = torrentsHaveSameShareRatio ? shareRatio : "";
330 id: "shareRatioPage",
331 icon: "images/qbittorrent-tray.svg",
332 title: "QBT_TR(Torrent Upload/Download Ratio Limiting)QBT_TR[CONTEXT=UpDownRatioDialog]",
333 loadMethod: "iframe",
334 contentURL: new URI("shareratio.html").setData("hashes", hashes.join("|")).setData("orig", orig).toString(),
338 paddingHorizontal: 0,
345 toggleSequentialDownloadFN = function() {
346 const hashes = torrentsTable.selectedRowsIds();
349 url: "api/v2/torrents/toggleSequentialDownload",
352 hashes: hashes.join("|")
359 toggleFirstLastPiecePrioFN = function() {
360 const hashes = torrentsTable.selectedRowsIds();
363 url: "api/v2/torrents/toggleFirstLastPiecePrio",
366 hashes: hashes.join("|")
373 setSuperSeedingFN = function(val) {
374 const hashes = torrentsTable.selectedRowsIds();
377 url: "api/v2/torrents/setSuperSeeding",
381 hashes: hashes.join("|")
388 setForceStartFN = function() {
389 const hashes = torrentsTable.selectedRowsIds();
392 url: "api/v2/torrents/setForceStart",
396 hashes: hashes.join("|")
403 globalDownloadLimitFN = function() {
405 id: "downloadLimitPage",
406 icon: "images/qbittorrent-tray.svg",
407 title: "QBT_TR(Global Download Speed Limit)QBT_TR[CONTEXT=MainWindow]",
408 loadMethod: "iframe",
409 contentURL: new URI("downloadlimit.html").setData("hashes", "global").toString(),
414 paddingHorizontal: 0,
420 StatisticsLinkFN = function() {
421 const id = "statisticspage";
424 icon: "images/qbittorrent-tray.svg",
425 title: "QBT_TR(Statistics)QBT_TR[CONTEXT=StatsDialog]",
427 contentURL: new URI("views/statistics.html").toString(),
430 width: loadWindowWidth(id, 285),
431 height: loadWindowHeight(id, 415),
432 onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
438 downloadLimitFN = function() {
439 const hashes = torrentsTable.selectedRowsIds();
442 id: "downloadLimitPage",
443 icon: "images/qbittorrent-tray.svg",
444 title: "QBT_TR(Torrent Download Speed Limiting)QBT_TR[CONTEXT=TransferListWidget]",
445 loadMethod: "iframe",
446 contentURL: new URI("downloadlimit.html").setData("hashes", hashes.join("|")).toString(),
451 paddingHorizontal: 0,
458 deleteFN = function(forceDeleteFiles = false) {
459 const hashes = torrentsTable.selectedRowsIds();
460 if (hashes.length > 0) {
461 if (window.qBittorrent.Cache.preferences.get().confirm_torrent_deletion) {
463 ...window.qBittorrent.Dialog.baseModalOptions,
464 id: "confirmDeletionPage",
465 title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
468 forceDeleteFiles: forceDeleteFiles
470 contentURL: "views/confirmdeletion.html",
471 onContentLoaded: function(w) {
472 MochaUI.resizeWindow(w, { centered: true });
473 MochaUI.centerWindow(w);
475 onCloseComplete: function() {
476 // make sure overlay is properly hidden upon modal closing
477 document.getElementById("modalOverlay").style.display = "none";
483 url: "api/v2/torrents/delete",
486 hashes: hashes.join("|"),
487 deleteFiles: forceDeleteFiles
489 onSuccess: function() {
490 torrentsTable.deselectAll();
493 onFailure: function() {
494 alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
501 addClickEvent("delete", (e) => {
507 stopFN = function() {
508 const hashes = torrentsTable.selectedRowsIds();
511 url: "api/v2/torrents/stop",
514 hashes: hashes.join("|")
521 startFN = function() {
522 const hashes = torrentsTable.selectedRowsIds();
525 url: "api/v2/torrents/start",
528 hashes: hashes.join("|")
535 autoTorrentManagementFN = function() {
536 const hashes = torrentsTable.selectedRowsIds();
539 hashes.each((hash, index) => {
540 const row = torrentsTable.getRow(hash);
541 if (!row.full_data.auto_tmm)
545 url: "api/v2/torrents/setAutoManagement",
548 hashes: hashes.join("|"),
556 recheckFN = function() {
557 const hashes = torrentsTable.selectedRowsIds();
558 if (hashes.length > 0) {
559 if (window.qBittorrent.Cache.preferences.get().confirm_torrent_recheck) {
561 ...window.qBittorrent.Dialog.baseModalOptions,
562 id: "confirmRecheckDialog",
563 title: "QBT_TR(Recheck confirmation)QBT_TR[CONTEXT=confirmRecheckDialog]",
564 data: { hashes: hashes },
565 contentURL: "views/confirmRecheck.html"
570 url: "api/v2/torrents/recheck",
573 "hashes": hashes.join("|"),
575 onSuccess: function() {
578 onFailure: function() {
579 alert("QBT_TR(Unable to recheck torrents.)QBT_TR[CONTEXT=HttpServer]");
586 reannounceFN = function() {
587 const hashes = torrentsTable.selectedRowsIds();
590 url: "api/v2/torrents/reannounce",
593 hashes: hashes.join("|"),
600 setLocationFN = function() {
601 const hashes = torrentsTable.selectedRowsIds();
603 const hash = hashes[0];
604 const row = torrentsTable.getRow(hash);
607 id: "setLocationPage",
608 icon: "images/qbittorrent-tray.svg",
609 title: "QBT_TR(Set location)QBT_TR[CONTEXT=TransferListWidget]",
610 loadMethod: "iframe",
611 contentURL: new URI("setlocation.html").setData("hashes", hashes.join("|")).setData("path", encodeURIComponent(row.full_data.save_path)).toString(),
616 paddingHorizontal: 0,
623 renameFN = function() {
624 const hashes = torrentsTable.selectedRowsIds();
625 if (hashes.length === 1) {
626 const hash = hashes[0];
627 const row = torrentsTable.getRow(hash);
631 icon: "images/qbittorrent-tray.svg",
632 title: "QBT_TR(Rename)QBT_TR[CONTEXT=TransferListWidget]",
633 loadMethod: "iframe",
634 contentURL: new URI("rename.html").setData("hash", hash).setData("name", row.full_data.name).toString(),
639 paddingHorizontal: 0,
647 renameFilesFN = function() {
648 const hashes = torrentsTable.selectedRowsIds();
649 if (hashes.length === 1) {
650 const hash = hashes[0];
651 const row = torrentsTable.getRow(hash);
654 id: "multiRenamePage",
655 icon: "images/qbittorrent-tray.svg",
656 title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TransferListWidget]",
657 data: { hash: hash, selectedRows: [] },
659 contentURL: "rename_files.html",
664 paddingHorizontal: 0,
667 resizeLimit: { "x": [800], "y": [420] }
673 torrentNewCategoryFN = function() {
674 const action = "set";
675 const hashes = torrentsTable.selectedRowsIds();
678 id: "newCategoryPage",
679 icon: "images/qbittorrent-tray.svg",
680 title: "QBT_TR(New Category)QBT_TR[CONTEXT=TransferListWidget]",
681 loadMethod: "iframe",
682 contentURL: new URI("newcategory.html").setData("action", action).setData("hashes", hashes.join("|")).toString(),
687 paddingHorizontal: 0,
694 torrentSetCategoryFN = function(categoryHash) {
695 const hashes = torrentsTable.selectedRowsIds();
696 if (hashes.length <= 0)
699 const categoryName = category_list.has(categoryHash)
700 ? category_list.get(categoryHash).name
703 url: "api/v2/torrents/setCategory",
706 hashes: hashes.join("|"),
707 category: categoryName
709 onSuccess: function() {
715 createCategoryFN = function() {
716 const action = "create";
718 id: "newCategoryPage",
719 icon: "images/qbittorrent-tray.svg",
720 title: "QBT_TR(New Category)QBT_TR[CONTEXT=CategoryFilterWidget]",
721 loadMethod: "iframe",
722 contentURL: new URI("newcategory.html").setData("action", action).toString(),
727 paddingHorizontal: 0,
733 createSubcategoryFN = function(categoryHash) {
734 const action = "createSubcategory";
735 const categoryName = category_list.get(categoryHash).name + "/";
737 id: "newSubcategoryPage",
738 icon: "images/qbittorrent-tray.svg",
739 title: "QBT_TR(New Category)QBT_TR[CONTEXT=CategoryFilterWidget]",
740 loadMethod: "iframe",
741 contentURL: new URI("newcategory.html").setData("action", action).setData("categoryName", categoryName).toString(),
746 paddingHorizontal: 0,
752 editCategoryFN = function(categoryHash) {
753 const action = "edit";
754 const category = category_list.get(categoryHash);
756 id: "editCategoryPage",
757 icon: "images/qbittorrent-tray.svg",
758 title: "QBT_TR(Edit Category)QBT_TR[CONTEXT=TransferListWidget]",
759 loadMethod: "iframe",
760 contentURL: new URI("newcategory.html").setData("action", action).setData("categoryName", category.name).setData("savePath", category.savePath).toString(),
765 paddingHorizontal: 0,
771 removeCategoryFN = function(categoryHash) {
772 const categoryName = category_list.get(categoryHash).name;
774 url: "api/v2/torrents/removeCategories",
777 categories: categoryName
779 onSuccess: function() {
780 setCategoryFilter(CATEGORIES_ALL);
786 deleteUnusedCategoriesFN = function() {
787 const categories = [];
788 category_list.forEach((category, hash) => {
789 if (torrentsTable.getFilteredTorrentsNumber("all", hash, TAGS_ALL, TRACKERS_ALL) === 0)
790 categories.push(category.name);
794 url: "api/v2/torrents/removeCategories",
797 categories: categories.join("\n")
799 onSuccess: function() {
800 setCategoryFilter(CATEGORIES_ALL);
806 startTorrentsByCategoryFN = function(categoryHash) {
807 const hashes = torrentsTable.getFilteredTorrentsHashes("all", categoryHash, TAGS_ALL, TRACKERS_ALL);
810 url: "api/v2/torrents/start",
813 hashes: hashes.join("|")
820 stopTorrentsByCategoryFN = function(categoryHash) {
821 const hashes = torrentsTable.getFilteredTorrentsHashes("all", categoryHash, TAGS_ALL, TRACKERS_ALL);
824 url: "api/v2/torrents/stop",
827 hashes: hashes.join("|")
834 deleteTorrentsByCategoryFN = function(categoryHash) {
835 const hashes = torrentsTable.getFilteredTorrentsHashes("all", categoryHash, TAGS_ALL, TRACKERS_ALL);
836 if (hashes.length > 0) {
837 if (window.qBittorrent.Cache.preferences.get().confirm_torrent_deletion) {
839 ...window.qBittorrent.Dialog.baseModalOptions,
840 id: "confirmDeletionPage",
841 title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
842 data: { hashes: hashes },
843 contentURL: "views/confirmdeletion.html",
844 onContentLoaded: function(w) {
845 MochaUI.resizeWindow(w, { centered: true });
846 MochaUI.centerWindow(w);
848 onCloseComplete: function() {
849 // make sure overlay is properly hidden upon modal closing
850 document.getElementById("modalOverlay").style.display = "none";
856 url: "api/v2/torrents/delete",
859 hashes: hashes.join("|"),
862 onSuccess: function() {
863 torrentsTable.deselectAll();
866 onFailure: function() {
867 alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
874 torrentAddTagsFN = function() {
875 const action = "set";
876 const hashes = torrentsTable.selectedRowsIds();
880 icon: "images/qbittorrent-tray.svg",
881 title: "QBT_TR(Add tags)QBT_TR[CONTEXT=TransferListWidget]",
882 loadMethod: "iframe",
883 contentURL: new URI("newtag.html").setData("action", action).setData("hashes", hashes.join("|")).toString(),
888 paddingHorizontal: 0,
895 torrentSetTagsFN = function(tagHash, isSet) {
896 const hashes = torrentsTable.selectedRowsIds();
897 if (hashes.length <= 0)
900 const tagName = tagList.has(tagHash) ? tagList.get(tagHash).name : "";
902 url: (isSet ? "api/v2/torrents/addTags" : "api/v2/torrents/removeTags"),
905 hashes: hashes.join("|"),
911 torrentRemoveAllTagsFN = function() {
912 const hashes = torrentsTable.selectedRowsIds();
915 url: ("api/v2/torrents/removeTags"),
918 hashes: hashes.join("|"),
924 createTagFN = function() {
925 const action = "create";
928 icon: "images/qbittorrent-tray.svg",
929 title: "QBT_TR(New Tag)QBT_TR[CONTEXT=TagFilterWidget]",
930 loadMethod: "iframe",
931 contentURL: new URI("newtag.html").setData("action", action).toString(),
936 paddingHorizontal: 0,
943 removeTagFN = function(tagHash) {
944 const tagName = tagList.get(tagHash).name;
946 url: "api/v2/torrents/deleteTags",
952 setTagFilter(TAGS_ALL);
955 deleteUnusedTagsFN = function() {
957 tagList.forEach((tag, hash) => {
958 if (torrentsTable.getFilteredTorrentsNumber("all", CATEGORIES_ALL, hash, TRACKERS_ALL) === 0)
962 url: "api/v2/torrents/deleteTags",
968 setTagFilter(TAGS_ALL);
971 startTorrentsByTagFN = function(tagHash) {
972 const hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, tagHash, TRACKERS_ALL);
975 url: "api/v2/torrents/start",
978 hashes: hashes.join("|")
985 stopTorrentsByTagFN = function(tagHash) {
986 const hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, tagHash, TRACKERS_ALL);
989 url: "api/v2/torrents/stop",
992 hashes: hashes.join("|")
999 deleteTorrentsByTagFN = function(tagHash) {
1000 const hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, tagHash, TRACKERS_ALL);
1001 if (hashes.length > 0) {
1002 if (window.qBittorrent.Cache.preferences.get().confirm_torrent_deletion) {
1004 ...window.qBittorrent.Dialog.baseModalOptions,
1005 id: "confirmDeletionPage",
1006 title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
1007 data: { hashes: hashes },
1008 contentURL: "views/confirmdeletion.html",
1009 onContentLoaded: function(w) {
1010 MochaUI.resizeWindow(w, { centered: true });
1011 MochaUI.centerWindow(w);
1013 onCloseComplete: function() {
1014 // make sure overlay is properly hidden upon modal closing
1015 document.getElementById("modalOverlay").style.display = "none";
1021 url: "api/v2/torrents/delete",
1024 hashes: hashes.join("|"),
1027 onSuccess: function() {
1028 torrentsTable.deselectAll();
1031 onFailure: function() {
1032 alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
1039 startTorrentsByTrackerFN = function(trackerHash) {
1040 const trackerHashInt = Number.parseInt(trackerHash, 10);
1042 switch (trackerHashInt) {
1044 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
1046 case TRACKERS_TRACKERLESS:
1047 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
1050 const uniqueTorrents = new Set();
1051 for (const torrents of trackerList.get(trackerHashInt).trackerTorrentMap.values()) {
1052 for (const torrent of torrents)
1053 uniqueTorrents.add(torrent);
1055 hashes = [...uniqueTorrents];
1060 if (hashes.length > 0) {
1062 url: "api/v2/torrents/start",
1065 hashes: hashes.join("|")
1072 stopTorrentsByTrackerFN = function(trackerHash) {
1073 const trackerHashInt = Number.parseInt(trackerHash, 10);
1075 switch (trackerHashInt) {
1077 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
1079 case TRACKERS_TRACKERLESS:
1080 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
1083 const uniqueTorrents = new Set();
1084 for (const torrents of trackerList.get(trackerHashInt).trackerTorrentMap.values()) {
1085 for (const torrent of torrents)
1086 uniqueTorrents.add(torrent);
1088 hashes = [...uniqueTorrents];
1093 if (hashes.length) {
1095 url: "api/v2/torrents/stop",
1098 hashes: hashes.join("|")
1105 deleteTorrentsByTrackerFN = function(trackerHash) {
1106 const trackerHashInt = Number.parseInt(trackerHash, 10);
1108 switch (trackerHashInt) {
1110 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
1112 case TRACKERS_TRACKERLESS:
1113 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
1116 const uniqueTorrents = new Set();
1117 for (const torrents of trackerList.get(trackerHashInt).trackerTorrentMap.values()) {
1118 for (const torrent of torrents)
1119 uniqueTorrents.add(torrent);
1121 hashes = [...uniqueTorrents];
1126 if (hashes.length > 0) {
1127 if (window.qBittorrent.Cache.preferences.get().confirm_torrent_deletion) {
1129 ...window.qBittorrent.Dialog.baseModalOptions,
1130 id: "confirmDeletionPage",
1131 title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
1134 filterList: "tracker"
1136 contentURL: "views/confirmdeletion.html",
1137 onContentLoaded: function(w) {
1138 MochaUI.resizeWindow(w, { centered: true });
1139 MochaUI.centerWindow(w);
1141 onCloseComplete: function() {
1142 // make sure overlay is properly hidden upon modal closing
1143 document.getElementById("modalOverlay").style.display = "none";
1149 url: "api/v2/torrents/delete",
1152 hashes: hashes.join("|"),
1155 onSuccess: function() {
1156 torrentsTable.deselectAll();
1157 setTrackerFilter(TRACKERS_ALL);
1160 onFailure: function() {
1161 alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
1168 deleteTrackerFN = function(trackerHash) {
1169 const trackerHashInt = Number.parseInt(trackerHash, 10);
1170 if ((trackerHashInt === TRACKERS_ALL) || (trackerHashInt === TRACKERS_TRACKERLESS))
1173 const tracker = trackerList.get(trackerHashInt);
1174 const host = tracker.host;
1175 const urls = [...tracker.trackerTorrentMap.keys()];
1177 new MochaUI.Window({
1178 id: "confirmDeletionPage",
1179 title: "QBT_TR(Remove tracker)QBT_TR[CONTEXT=confirmDeletionDlg]",
1180 loadMethod: "iframe",
1181 contentURL: new URI("confirmtrackerdeletion.html").setData("host", host).setData("urls", urls.map(encodeURIComponent).join("|")).toString(),
1188 onCloseComplete: function() {
1190 setTrackerFilter(TRACKERS_ALL);
1195 copyNameFN = function() {
1196 const selectedRows = torrentsTable.selectedRowsIds();
1198 if (selectedRows.length > 0) {
1199 const rows = torrentsTable.getFilteredAndSortedRows();
1200 for (let i = 0; i < selectedRows.length; ++i) {
1201 const hash = selectedRows[i];
1202 names.push(rows[hash].full_data.name);
1205 return names.join("\n");
1208 copyInfohashFN = function(policy) {
1209 const selectedRows = torrentsTable.selectedRowsIds();
1210 const infohashes = [];
1211 if (selectedRows.length > 0) {
1212 const rows = torrentsTable.getFilteredAndSortedRows();
1215 for (const id of selectedRows) {
1216 const infohash = rows[id].full_data.infohash_v1;
1217 if (infohash !== "")
1218 infohashes.push(infohash);
1222 for (const id of selectedRows) {
1223 const infohash = rows[id].full_data.infohash_v2;
1224 if (infohash !== "")
1225 infohashes.push(infohash);
1230 return infohashes.join("\n");
1233 copyMagnetLinkFN = function() {
1234 const selectedRows = torrentsTable.selectedRowsIds();
1236 if (selectedRows.length > 0) {
1237 const rows = torrentsTable.getFilteredAndSortedRows();
1238 for (let i = 0; i < selectedRows.length; ++i) {
1239 const hash = selectedRows[i];
1240 magnets.push(rows[hash].full_data.magnet_uri);
1243 return magnets.join("\n");
1246 copyIdFN = function() {
1247 return torrentsTable.selectedRowsIds().join("\n");
1250 copyCommentFN = function() {
1251 const selectedRows = torrentsTable.selectedRowsIds();
1252 const comments = [];
1253 if (selectedRows.length > 0) {
1254 const rows = torrentsTable.getFilteredAndSortedRows();
1255 for (let i = 0; i < selectedRows.length; ++i) {
1256 const hash = selectedRows[i];
1257 const comment = rows[hash].full_data.comment;
1258 if (comment && (comment !== ""))
1259 comments.push(comment);
1262 return comments.join("\n---------\n");
1265 exportTorrentFN = async function() {
1266 const hashes = torrentsTable.selectedRowsIds();
1267 for (const hash of hashes) {
1268 const row = torrentsTable.getRow(hash);
1272 const name = row.full_data.name;
1273 const url = new URI("api/v2/torrents/export");
1274 url.setData("hash", hash);
1276 // download response to file
1277 const element = document.createElement("a");
1279 element.download = (name + ".torrent");
1280 document.body.appendChild(element);
1282 document.body.removeChild(element);
1284 // https://stackoverflow.com/questions/53560991/automatic-file-downloads-limited-to-10-files-on-chrome-browser
1285 await window.qBittorrent.Misc.sleep(200);
1289 addClickEvent("stopAll", (e) => {
1291 e.stopPropagation();
1293 if (confirm("QBT_TR(Would you like to stop all torrents?)QBT_TR[CONTEXT=MainWindow]")) {
1295 url: "api/v2/torrents/stop",
1305 addClickEvent("startAll", (e) => {
1307 e.stopPropagation();
1309 if (confirm("QBT_TR(Would you like to start all torrents?)QBT_TR[CONTEXT=MainWindow]")) {
1311 url: "api/v2/torrents/start",
1321 ["stop", "start", "recheck"].each((item) => {
1322 addClickEvent(item, (e) => {
1324 e.stopPropagation();
1326 const hashes = torrentsTable.selectedRowsIds();
1327 if (hashes.length) {
1328 hashes.each((hash, index) => {
1330 url: "api/v2/torrents/" + item,
1342 ["decreasePrio", "increasePrio", "topPrio", "bottomPrio"].each((item) => {
1343 addClickEvent(item, (e) => {
1345 e.stopPropagation();
1346 setQueuePositionFN(item);
1350 setQueuePositionFN = function(cmd) {
1351 const hashes = torrentsTable.selectedRowsIds();
1352 if (hashes.length) {
1354 url: "api/v2/torrents/" + cmd,
1357 hashes: hashes.join("|")
1364 addClickEvent("about", (e) => {
1366 e.stopPropagation();
1368 const id = "aboutpage";
1369 new MochaUI.Window({
1371 icon: "images/qbittorrent-tray.svg",
1372 title: "QBT_TR(About qBittorrent)QBT_TR[CONTEXT=AboutDialog]",
1374 contentURL: new URI("views/about.html").toString(),
1376 css: ["css/Tabs.css"]
1379 toolbarURL: "views/aboutToolbar.html",
1381 width: loadWindowWidth(id, 550),
1382 height: loadWindowHeight(id, 360),
1383 onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
1389 addClickEvent("logout", (e) => {
1391 e.stopPropagation();
1394 url: "api/v2/auth/logout",
1396 onSuccess: function() {
1397 window.location.reload(true);
1402 addClickEvent("shutdown", (e) => {
1404 e.stopPropagation();
1406 if (confirm("QBT_TR(Are you sure you want to quit qBittorrent?)QBT_TR[CONTEXT=MainWindow]")) {
1408 url: "api/v2/app/shutdown",
1410 onSuccess: function() {
1411 const shutdownMessage = "QBT_TR(%1 has been shutdown)QBT_TR[CONTEXT=HttpServer]".replace("%1", window.qBittorrent.Client.mainTitle());
1412 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>`);
1415 window.qBittorrent.Client.stop();
1421 // Deactivate menu header links
1422 $$("a.returnFalse").each((el) => {
1423 el.addEventListener("click", (e) => {
1425 e.stopPropagation();