WebUI: Improve statistics window
[qBittorrent.git] / src / webui / www / private / scripts / mocha-init.js
blob2f24ed2e2bfff6713b8c7bd55e1117f0f1d7349b
1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2008  Christophe Dumez <chris@qbittorrent.org>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18  *
19  * In addition, as a special exception, the copyright holders give permission to
20  * link this program with the OpenSSL project's "OpenSSL" library (or with
21  * modified versions of it that use the same license as the "OpenSSL" library),
22  * and distribute the linked executables. You must obey the GNU General Public
23  * License in all respects for all of the code used other than "OpenSSL".  If you
24  * modify file(s), you may extend this exception to your version of the file(s),
25  * but you are not obligated to do so. If you do not wish to do so, delete this
26  * exception statement from your version.
27  */
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    ----------------------------------------------------------------- */
39 "use strict";
41 window.qBittorrent ??= {};
42 window.qBittorrent.Dialog ??= (() => {
43     const exports = () => {
44         return {
45             baseModalOptions: baseModalOptions
46         };
47     };
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) => {
54             if (frozen.has(obj))
55                 return;
57             frozen.add(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);
64             }
65             Object.freeze(obj);
66         };
68         deepFreezeSafe(obj);
69     };
71     const baseModalOptions = Object.assign(Object.create(null), {
72         addClass: "modalDialog",
73         collapsible: false,
74         cornerRadius: 5,
75         draggable: true,
76         footerHeight: 20,
77         icon: "images/qbittorrent-tray.svg",
78         loadMethod: "xhr",
79         maximizable: false,
80         method: "post",
81         minimizable: false,
82         padding: {
83             top: 15,
84             right: 10,
85             bottom: 15,
86             left: 5
87         },
88         resizable: true,
89         width: 480,
90         onCloseComplete: function() {
91             // make sure overlay is properly hidden upon modal closing
92             document.getElementById("modalOverlay").style.display = "none";
93         }
94     });
96     deepFreeze(baseModalOptions);
98     return exports();
99 })();
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);
163     };
165     loadWindowWidth = function(windowId, defaultValue) {
166         return LocalPreferences.get("window_" + windowId + "_width", defaultValue);
167     };
169     loadWindowHeight = function(windowId, defaultValue) {
170         return LocalPreferences.get("window_" + windowId + "_height", defaultValue);
171     };
173     function addClickEvent(el, fn) {
174         ["Link", "Button"].each((item) => {
175             if ($(el + item))
176                 $(el + item).addEventListener("click", fn);
177         });
178     }
180     addClickEvent("download", (e) => {
181         e.preventDefault();
182         e.stopPropagation();
183         showDownloadPage();
184     });
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("|"));
193         new MochaUI.Window({
194             id: id,
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
200             scrollbars: true,
201             maximizable: false,
202             closable: true,
203             paddingVertical: 0,
204             paddingHorizontal: 0,
205             width: loadWindowWidth(id, 500),
206             height: loadWindowHeight(id, 600),
207             onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
208                 saveWindowSize(id);
209             })
210         });
211         updateMainData();
212     };
214     addClickEvent("preferences", (e) => {
215         e.preventDefault();
216         e.stopPropagation();
218         const id = "preferencesPage";
219         new MochaUI.Window({
220             id: id,
221             icon: "images/qbittorrent-tray.svg",
222             title: "QBT_TR(Options)QBT_TR[CONTEXT=OptionsDialog]",
223             loadMethod: "xhr",
224             toolbar: true,
225             contentURL: new URI("views/preferences.html").toString(),
226             require: {
227                 css: ["css/Tabs.css"]
228             },
229             toolbarURL: "views/preferencesToolbar.html",
230             maximizable: false,
231             closable: true,
232             paddingVertical: 0,
233             paddingHorizontal: 0,
234             width: loadWindowWidth(id, 700),
235             height: loadWindowHeight(id, 600),
236             onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
237                 saveWindowSize(id);
238             })
239         });
240     });
242     addClickEvent("upload", (e) => {
243         e.preventDefault();
244         e.stopPropagation();
246         const id = "uploadPage";
247         new MochaUI.Window({
248             id: id,
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
254             scrollbars: true,
255             maximizable: false,
256             paddingVertical: 0,
257             paddingHorizontal: 0,
258             width: loadWindowWidth(id, 500),
259             height: loadWindowHeight(id, 460),
260             onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
261                 saveWindowSize(id);
262             })
263         });
264         updateMainData();
265     });
267     globalUploadLimitFN = function() {
268         new MochaUI.Window({
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(),
274             scrollbars: false,
275             resizable: false,
276             maximizable: false,
277             paddingVertical: 0,
278             paddingHorizontal: 0,
279             width: 424,
280             height: 80
281         });
282     };
284     uploadLimitFN = function() {
285         const hashes = torrentsTable.selectedRowsIds();
286         if (hashes.length) {
287             new MochaUI.Window({
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(),
293                 scrollbars: false,
294                 resizable: false,
295                 maximizable: false,
296                 paddingVertical: 0,
297                 paddingHorizontal: 0,
298                 width: 424,
299                 height: 80
300             });
301         }
302     };
304     shareRatioFN = function() {
305         const hashes = torrentsTable.selectedRowsIds();
306         if (hashes.length) {
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;
317                 // initialize value
318                 if (shareRatio === null)
319                     shareRatio = origValues;
321                 if (origValues !== shareRatio) {
322                     torrentsHaveSameShareRatio = false;
323                     break;
324                 }
325             }
327             // if all torrents have same share ratio, display that share ratio. else use the default
328             const orig = torrentsHaveSameShareRatio ? shareRatio : "";
329             new MochaUI.Window({
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(),
335                 scrollbars: false,
336                 maximizable: false,
337                 paddingVertical: 0,
338                 paddingHorizontal: 0,
339                 width: 424,
340                 height: 200
341             });
342         }
343     };
345     toggleSequentialDownloadFN = function() {
346         const hashes = torrentsTable.selectedRowsIds();
347         if (hashes.length) {
348             new Request({
349                 url: "api/v2/torrents/toggleSequentialDownload",
350                 method: "post",
351                 data: {
352                     hashes: hashes.join("|")
353                 }
354             }).send();
355             updateMainData();
356         }
357     };
359     toggleFirstLastPiecePrioFN = function() {
360         const hashes = torrentsTable.selectedRowsIds();
361         if (hashes.length) {
362             new Request({
363                 url: "api/v2/torrents/toggleFirstLastPiecePrio",
364                 method: "post",
365                 data: {
366                     hashes: hashes.join("|")
367                 }
368             }).send();
369             updateMainData();
370         }
371     };
373     setSuperSeedingFN = function(val) {
374         const hashes = torrentsTable.selectedRowsIds();
375         if (hashes.length) {
376             new Request({
377                 url: "api/v2/torrents/setSuperSeeding",
378                 method: "post",
379                 data: {
380                     value: val,
381                     hashes: hashes.join("|")
382                 }
383             }).send();
384             updateMainData();
385         }
386     };
388     setForceStartFN = function() {
389         const hashes = torrentsTable.selectedRowsIds();
390         if (hashes.length) {
391             new Request({
392                 url: "api/v2/torrents/setForceStart",
393                 method: "post",
394                 data: {
395                     value: "true",
396                     hashes: hashes.join("|")
397                 }
398             }).send();
399             updateMainData();
400         }
401     };
403     globalDownloadLimitFN = function() {
404         new MochaUI.Window({
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(),
410             scrollbars: false,
411             resizable: false,
412             maximizable: false,
413             paddingVertical: 0,
414             paddingHorizontal: 0,
415             width: 424,
416             height: 80
417         });
418     };
420     StatisticsLinkFN = function() {
421         const id = "statisticspage";
422         new MochaUI.Window({
423             id: id,
424             icon: "images/qbittorrent-tray.svg",
425             title: "QBT_TR(Statistics)QBT_TR[CONTEXT=StatsDialog]",
426             loadMethod: "xhr",
427             contentURL: new URI("views/statistics.html").toString(),
428             maximizable: false,
429             padding: 10,
430             width: loadWindowWidth(id, 285),
431             height: loadWindowHeight(id, 415),
432             onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
433                 saveWindowSize(id);
434             })
435         });
436     };
438     downloadLimitFN = function() {
439         const hashes = torrentsTable.selectedRowsIds();
440         if (hashes.length) {
441             new MochaUI.Window({
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(),
447                 scrollbars: false,
448                 resizable: false,
449                 maximizable: false,
450                 paddingVertical: 0,
451                 paddingHorizontal: 0,
452                 width: 424,
453                 height: 80
454             });
455         }
456     };
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) {
462                 new MochaUI.Modal({
463                     ...window.qBittorrent.Dialog.baseModalOptions,
464                     id: "confirmDeletionPage",
465                     title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
466                     data: {
467                         hashes: hashes,
468                         forceDeleteFiles: forceDeleteFiles
469                     },
470                     contentURL: "views/confirmdeletion.html",
471                     onContentLoaded: function(w) {
472                         MochaUI.resizeWindow(w, { centered: true });
473                         MochaUI.centerWindow(w);
474                     },
475                     onCloseComplete: function() {
476                         // make sure overlay is properly hidden upon modal closing
477                         document.getElementById("modalOverlay").style.display = "none";
478                     }
479                 });
480             }
481             else {
482                 new Request({
483                     url: "api/v2/torrents/delete",
484                     method: "post",
485                     data: {
486                         hashes: hashes.join("|"),
487                         deleteFiles: forceDeleteFiles
488                     },
489                     onSuccess: function() {
490                         torrentsTable.deselectAll();
491                         updateMainData();
492                     },
493                     onFailure: function() {
494                         alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
495                     }
496                 }).send();
497             }
498         }
499     };
501     addClickEvent("delete", (e) => {
502         e.preventDefault();
503         e.stopPropagation();
504         deleteFN();
505     });
507     stopFN = function() {
508         const hashes = torrentsTable.selectedRowsIds();
509         if (hashes.length) {
510             new Request({
511                 url: "api/v2/torrents/stop",
512                 method: "post",
513                 data: {
514                     hashes: hashes.join("|")
515                 }
516             }).send();
517             updateMainData();
518         }
519     };
521     startFN = function() {
522         const hashes = torrentsTable.selectedRowsIds();
523         if (hashes.length) {
524             new Request({
525                 url: "api/v2/torrents/start",
526                 method: "post",
527                 data: {
528                     hashes: hashes.join("|")
529                 }
530             }).send();
531             updateMainData();
532         }
533     };
535     autoTorrentManagementFN = function() {
536         const hashes = torrentsTable.selectedRowsIds();
537         if (hashes.length) {
538             let enable = false;
539             hashes.each((hash, index) => {
540                 const row = torrentsTable.getRow(hash);
541                 if (!row.full_data.auto_tmm)
542                     enable = true;
543             });
544             new Request({
545                 url: "api/v2/torrents/setAutoManagement",
546                 method: "post",
547                 data: {
548                     hashes: hashes.join("|"),
549                     enable: enable
550                 }
551             }).send();
552             updateMainData();
553         }
554     };
556     recheckFN = function() {
557         const hashes = torrentsTable.selectedRowsIds();
558         if (hashes.length > 0) {
559             if (window.qBittorrent.Cache.preferences.get().confirm_torrent_recheck) {
560                 new MochaUI.Modal({
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"
566                 });
567             }
568             else {
569                 new Request({
570                     url: "api/v2/torrents/recheck",
571                     method: "post",
572                     data: {
573                         "hashes": hashes.join("|"),
574                     },
575                     onSuccess: function() {
576                         updateMainData();
577                     },
578                     onFailure: function() {
579                         alert("QBT_TR(Unable to recheck torrents.)QBT_TR[CONTEXT=HttpServer]");
580                     }
581                 }).send();
582             }
583         }
584     };
586     reannounceFN = function() {
587         const hashes = torrentsTable.selectedRowsIds();
588         if (hashes.length) {
589             new Request({
590                 url: "api/v2/torrents/reannounce",
591                 method: "post",
592                 data: {
593                     hashes: hashes.join("|"),
594                 }
595             }).send();
596             updateMainData();
597         }
598     };
600     setLocationFN = function() {
601         const hashes = torrentsTable.selectedRowsIds();
602         if (hashes.length) {
603             const hash = hashes[0];
604             const row = torrentsTable.getRow(hash);
606             new MochaUI.Window({
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(),
612                 scrollbars: false,
613                 resizable: true,
614                 maximizable: false,
615                 paddingVertical: 0,
616                 paddingHorizontal: 0,
617                 width: 400,
618                 height: 130
619             });
620         }
621     };
623     renameFN = function() {
624         const hashes = torrentsTable.selectedRowsIds();
625         if (hashes.length === 1) {
626             const hash = hashes[0];
627             const row = torrentsTable.getRow(hash);
628             if (row) {
629                 new MochaUI.Window({
630                     id: "renamePage",
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(),
635                     scrollbars: false,
636                     resizable: true,
637                     maximizable: false,
638                     paddingVertical: 0,
639                     paddingHorizontal: 0,
640                     width: 400,
641                     height: 100
642                 });
643             }
644         }
645     };
647     renameFilesFN = function() {
648         const hashes = torrentsTable.selectedRowsIds();
649         if (hashes.length === 1) {
650             const hash = hashes[0];
651             const row = torrentsTable.getRow(hash);
652             if (row) {
653                 new MochaUI.Window({
654                     id: "multiRenamePage",
655                     icon: "images/qbittorrent-tray.svg",
656                     title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TransferListWidget]",
657                     data: { hash: hash, selectedRows: [] },
658                     loadMethod: "xhr",
659                     contentURL: "rename_files.html",
660                     scrollbars: false,
661                     resizable: true,
662                     maximizable: false,
663                     paddingVertical: 0,
664                     paddingHorizontal: 0,
665                     width: 800,
666                     height: 420,
667                     resizeLimit: { "x": [800], "y": [420] }
668                 });
669             }
670         }
671     };
673     torrentNewCategoryFN = function() {
674         const action = "set";
675         const hashes = torrentsTable.selectedRowsIds();
676         if (hashes.length) {
677             new MochaUI.Window({
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(),
683                 scrollbars: false,
684                 resizable: true,
685                 maximizable: false,
686                 paddingVertical: 0,
687                 paddingHorizontal: 0,
688                 width: 400,
689                 height: 150
690             });
691         }
692     };
694     torrentSetCategoryFN = function(categoryHash) {
695         const hashes = torrentsTable.selectedRowsIds();
696         if (hashes.length <= 0)
697             return;
699         const categoryName = category_list.has(categoryHash)
700             ? category_list.get(categoryHash).name
701             : "";
702         new Request({
703             url: "api/v2/torrents/setCategory",
704             method: "post",
705             data: {
706                 hashes: hashes.join("|"),
707                 category: categoryName
708             },
709             onSuccess: function() {
710                 updateMainData();
711             }
712         }).send();
713     };
715     createCategoryFN = function() {
716         const action = "create";
717         new MochaUI.Window({
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(),
723             scrollbars: false,
724             resizable: true,
725             maximizable: false,
726             paddingVertical: 0,
727             paddingHorizontal: 0,
728             width: 400,
729             height: 150
730         });
731     };
733     createSubcategoryFN = function(categoryHash) {
734         const action = "createSubcategory";
735         const categoryName = category_list.get(categoryHash).name + "/";
736         new MochaUI.Window({
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(),
742             scrollbars: false,
743             resizable: true,
744             maximizable: false,
745             paddingVertical: 0,
746             paddingHorizontal: 0,
747             width: 400,
748             height: 150
749         });
750     };
752     editCategoryFN = function(categoryHash) {
753         const action = "edit";
754         const category = category_list.get(categoryHash);
755         new MochaUI.Window({
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(),
761             scrollbars: false,
762             resizable: true,
763             maximizable: false,
764             paddingVertical: 0,
765             paddingHorizontal: 0,
766             width: 400,
767             height: 150
768         });
769     };
771     removeCategoryFN = function(categoryHash) {
772         const categoryName = category_list.get(categoryHash).name;
773         new Request({
774             url: "api/v2/torrents/removeCategories",
775             method: "post",
776             data: {
777                 categories: categoryName
778             },
779             onSuccess: function() {
780                 setCategoryFilter(CATEGORIES_ALL);
781                 updateMainData();
782             }
783         }).send();
784     };
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);
791         });
793         new Request({
794             url: "api/v2/torrents/removeCategories",
795             method: "post",
796             data: {
797                 categories: categories.join("\n")
798             },
799             onSuccess: function() {
800                 setCategoryFilter(CATEGORIES_ALL);
801                 updateMainData();
802             }
803         }).send();
804     };
806     startTorrentsByCategoryFN = function(categoryHash) {
807         const hashes = torrentsTable.getFilteredTorrentsHashes("all", categoryHash, TAGS_ALL, TRACKERS_ALL);
808         if (hashes.length) {
809             new Request({
810                 url: "api/v2/torrents/start",
811                 method: "post",
812                 data: {
813                     hashes: hashes.join("|")
814                 }
815             }).send();
816             updateMainData();
817         }
818     };
820     stopTorrentsByCategoryFN = function(categoryHash) {
821         const hashes = torrentsTable.getFilteredTorrentsHashes("all", categoryHash, TAGS_ALL, TRACKERS_ALL);
822         if (hashes.length) {
823             new Request({
824                 url: "api/v2/torrents/stop",
825                 method: "post",
826                 data: {
827                     hashes: hashes.join("|")
828                 }
829             }).send();
830             updateMainData();
831         }
832     };
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) {
838                 new MochaUI.Modal({
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);
847                     },
848                     onCloseComplete: function() {
849                         // make sure overlay is properly hidden upon modal closing
850                         document.getElementById("modalOverlay").style.display = "none";
851                     }
852                 });
853             }
854             else {
855                 new Request({
856                     url: "api/v2/torrents/delete",
857                     method: "post",
858                     data: {
859                         hashes: hashes.join("|"),
860                         deleteFiles: false,
861                     },
862                     onSuccess: function() {
863                         torrentsTable.deselectAll();
864                         updateMainData();
865                     },
866                     onFailure: function() {
867                         alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
868                     }
869                 }).send();
870             }
871         }
872     };
874     torrentAddTagsFN = function() {
875         const action = "set";
876         const hashes = torrentsTable.selectedRowsIds();
877         if (hashes.length) {
878             new MochaUI.Window({
879                 id: "newTagPage",
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(),
884                 scrollbars: false,
885                 resizable: true,
886                 maximizable: false,
887                 paddingVertical: 0,
888                 paddingHorizontal: 0,
889                 width: 250,
890                 height: 100
891             });
892         }
893     };
895     torrentSetTagsFN = function(tagHash, isSet) {
896         const hashes = torrentsTable.selectedRowsIds();
897         if (hashes.length <= 0)
898             return;
900         const tagName = tagList.has(tagHash) ? tagList.get(tagHash).name : "";
901         new Request({
902             url: (isSet ? "api/v2/torrents/addTags" : "api/v2/torrents/removeTags"),
903             method: "post",
904             data: {
905                 hashes: hashes.join("|"),
906                 tags: tagName,
907             }
908         }).send();
909     };
911     torrentRemoveAllTagsFN = function() {
912         const hashes = torrentsTable.selectedRowsIds();
913         if (hashes.length) {
914             new Request({
915                 url: ("api/v2/torrents/removeTags"),
916                 method: "post",
917                 data: {
918                     hashes: hashes.join("|"),
919                 }
920             }).send();
921         }
922     };
924     createTagFN = function() {
925         const action = "create";
926         new MochaUI.Window({
927             id: "newTagPage",
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(),
932             scrollbars: false,
933             resizable: true,
934             maximizable: false,
935             paddingVertical: 0,
936             paddingHorizontal: 0,
937             width: 250,
938             height: 100
939         });
940         updateMainData();
941     };
943     removeTagFN = function(tagHash) {
944         const tagName = tagList.get(tagHash).name;
945         new Request({
946             url: "api/v2/torrents/deleteTags",
947             method: "post",
948             data: {
949                 tags: tagName
950             }
951         }).send();
952         setTagFilter(TAGS_ALL);
953     };
955     deleteUnusedTagsFN = function() {
956         const tags = [];
957         tagList.forEach((tag, hash) => {
958             if (torrentsTable.getFilteredTorrentsNumber("all", CATEGORIES_ALL, hash, TRACKERS_ALL) === 0)
959                 tags.push(tag.name);
960         });
961         new Request({
962             url: "api/v2/torrents/deleteTags",
963             method: "post",
964             data: {
965                 tags: tags.join(",")
966             }
967         }).send();
968         setTagFilter(TAGS_ALL);
969     };
971     startTorrentsByTagFN = function(tagHash) {
972         const hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, tagHash, TRACKERS_ALL);
973         if (hashes.length) {
974             new Request({
975                 url: "api/v2/torrents/start",
976                 method: "post",
977                 data: {
978                     hashes: hashes.join("|")
979                 }
980             }).send();
981             updateMainData();
982         }
983     };
985     stopTorrentsByTagFN = function(tagHash) {
986         const hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, tagHash, TRACKERS_ALL);
987         if (hashes.length) {
988             new Request({
989                 url: "api/v2/torrents/stop",
990                 method: "post",
991                 data: {
992                     hashes: hashes.join("|")
993                 }
994             }).send();
995             updateMainData();
996         }
997     };
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) {
1003                 new MochaUI.Modal({
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);
1012                     },
1013                     onCloseComplete: function() {
1014                         // make sure overlay is properly hidden upon modal closing
1015                         document.getElementById("modalOverlay").style.display = "none";
1016                     }
1017                 });
1018             }
1019             else {
1020                 new Request({
1021                     url: "api/v2/torrents/delete",
1022                     method: "post",
1023                     data: {
1024                         hashes: hashes.join("|"),
1025                         deleteFiles: false,
1026                     },
1027                     onSuccess: function() {
1028                         torrentsTable.deselectAll();
1029                         updateMainData();
1030                     },
1031                     onFailure: function() {
1032                         alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
1033                     }
1034                 }).send();
1035             }
1036         }
1037     };
1039     startTorrentsByTrackerFN = function(trackerHash) {
1040         const trackerHashInt = Number.parseInt(trackerHash, 10);
1041         let hashes = [];
1042         switch (trackerHashInt) {
1043             case TRACKERS_ALL:
1044                 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
1045                 break;
1046             case TRACKERS_TRACKERLESS:
1047                 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
1048                 break;
1049             default: {
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);
1054                 }
1055                 hashes = [...uniqueTorrents];
1056                 break;
1057             }
1058         }
1060         if (hashes.length > 0) {
1061             new Request({
1062                 url: "api/v2/torrents/start",
1063                 method: "post",
1064                 data: {
1065                     hashes: hashes.join("|")
1066                 }
1067             }).send();
1068             updateMainData();
1069         }
1070     };
1072     stopTorrentsByTrackerFN = function(trackerHash) {
1073         const trackerHashInt = Number.parseInt(trackerHash, 10);
1074         let hashes = [];
1075         switch (trackerHashInt) {
1076             case TRACKERS_ALL:
1077                 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
1078                 break;
1079             case TRACKERS_TRACKERLESS:
1080                 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
1081                 break;
1082             default: {
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);
1087                 }
1088                 hashes = [...uniqueTorrents];
1089                 break;
1090             }
1091         }
1093         if (hashes.length) {
1094             new Request({
1095                 url: "api/v2/torrents/stop",
1096                 method: "post",
1097                 data: {
1098                     hashes: hashes.join("|")
1099                 }
1100             }).send();
1101             updateMainData();
1102         }
1103     };
1105     deleteTorrentsByTrackerFN = function(trackerHash) {
1106         const trackerHashInt = Number.parseInt(trackerHash, 10);
1107         let hashes = [];
1108         switch (trackerHashInt) {
1109             case TRACKERS_ALL:
1110                 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
1111                 break;
1112             case TRACKERS_TRACKERLESS:
1113                 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
1114                 break;
1115             default: {
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);
1120                 }
1121                 hashes = [...uniqueTorrents];
1122                 break;
1123             }
1124         }
1126         if (hashes.length > 0) {
1127             if (window.qBittorrent.Cache.preferences.get().confirm_torrent_deletion) {
1128                 new MochaUI.Modal({
1129                     ...window.qBittorrent.Dialog.baseModalOptions,
1130                     id: "confirmDeletionPage",
1131                     title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
1132                     data: {
1133                         hashes: hashes,
1134                         filterList: "tracker"
1135                     },
1136                     contentURL: "views/confirmdeletion.html",
1137                     onContentLoaded: function(w) {
1138                         MochaUI.resizeWindow(w, { centered: true });
1139                         MochaUI.centerWindow(w);
1140                     },
1141                     onCloseComplete: function() {
1142                         // make sure overlay is properly hidden upon modal closing
1143                         document.getElementById("modalOverlay").style.display = "none";
1144                     }
1145                 });
1146             }
1147             else {
1148                 new Request({
1149                     url: "api/v2/torrents/delete",
1150                     method: "post",
1151                     data: {
1152                         hashes: hashes.join("|"),
1153                         deleteFiles: false,
1154                     },
1155                     onSuccess: function() {
1156                         torrentsTable.deselectAll();
1157                         setTrackerFilter(TRACKERS_ALL);
1158                         updateMainData();
1159                     },
1160                     onFailure: function() {
1161                         alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
1162                     },
1163                 }).send();
1164             }
1165         }
1166     };
1168     deleteTrackerFN = function(trackerHash) {
1169         const trackerHashInt = Number.parseInt(trackerHash, 10);
1170         if ((trackerHashInt === TRACKERS_ALL) || (trackerHashInt === TRACKERS_TRACKERLESS))
1171             return;
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(),
1182             scrollbars: false,
1183             resizable: true,
1184             maximizable: false,
1185             padding: 10,
1186             width: 424,
1187             height: 100,
1188             onCloseComplete: function() {
1189                 updateMainData();
1190                 setTrackerFilter(TRACKERS_ALL);
1191             }
1192         });
1193     };
1195     copyNameFN = function() {
1196         const selectedRows = torrentsTable.selectedRowsIds();
1197         const names = [];
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);
1203             }
1204         }
1205         return names.join("\n");
1206     };
1208     copyInfohashFN = function(policy) {
1209         const selectedRows = torrentsTable.selectedRowsIds();
1210         const infohashes = [];
1211         if (selectedRows.length > 0) {
1212             const rows = torrentsTable.getFilteredAndSortedRows();
1213             switch (policy) {
1214                 case 1:
1215                     for (const id of selectedRows) {
1216                         const infohash = rows[id].full_data.infohash_v1;
1217                         if (infohash !== "")
1218                             infohashes.push(infohash);
1219                     }
1220                     break;
1221                 case 2:
1222                     for (const id of selectedRows) {
1223                         const infohash = rows[id].full_data.infohash_v2;
1224                         if (infohash !== "")
1225                             infohashes.push(infohash);
1226                     }
1227                     break;
1228             }
1229         }
1230         return infohashes.join("\n");
1231     };
1233     copyMagnetLinkFN = function() {
1234         const selectedRows = torrentsTable.selectedRowsIds();
1235         const magnets = [];
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);
1241             }
1242         }
1243         return magnets.join("\n");
1244     };
1246     copyIdFN = function() {
1247         return torrentsTable.selectedRowsIds().join("\n");
1248     };
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);
1260             }
1261         }
1262         return comments.join("\n---------\n");
1263     };
1265     exportTorrentFN = async function() {
1266         const hashes = torrentsTable.selectedRowsIds();
1267         for (const hash of hashes) {
1268             const row = torrentsTable.getRow(hash);
1269             if (!row)
1270                 continue;
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");
1278             element.href = url;
1279             element.download = (name + ".torrent");
1280             document.body.appendChild(element);
1281             element.click();
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);
1286         }
1287     };
1289     addClickEvent("stopAll", (e) => {
1290         e.preventDefault();
1291         e.stopPropagation();
1293         if (confirm("QBT_TR(Would you like to stop all torrents?)QBT_TR[CONTEXT=MainWindow]")) {
1294             new Request({
1295                 url: "api/v2/torrents/stop",
1296                 method: "post",
1297                 data: {
1298                     hashes: "all"
1299                 }
1300             }).send();
1301             updateMainData();
1302         }
1303     });
1305     addClickEvent("startAll", (e) => {
1306         e.preventDefault();
1307         e.stopPropagation();
1309         if (confirm("QBT_TR(Would you like to start all torrents?)QBT_TR[CONTEXT=MainWindow]")) {
1310             new Request({
1311                 url: "api/v2/torrents/start",
1312                 method: "post",
1313                 data: {
1314                     hashes: "all"
1315                 }
1316             }).send();
1317             updateMainData();
1318         }
1319     });
1321     ["stop", "start", "recheck"].each((item) => {
1322         addClickEvent(item, (e) => {
1323             e.preventDefault();
1324             e.stopPropagation();
1326             const hashes = torrentsTable.selectedRowsIds();
1327             if (hashes.length) {
1328                 hashes.each((hash, index) => {
1329                     new Request({
1330                         url: "api/v2/torrents/" + item,
1331                         method: "post",
1332                         data: {
1333                             hashes: hash
1334                         }
1335                     }).send();
1336                 });
1337                 updateMainData();
1338             }
1339         });
1340     });
1342     ["decreasePrio", "increasePrio", "topPrio", "bottomPrio"].each((item) => {
1343         addClickEvent(item, (e) => {
1344             e.preventDefault();
1345             e.stopPropagation();
1346             setQueuePositionFN(item);
1347         });
1348     });
1350     setQueuePositionFN = function(cmd) {
1351         const hashes = torrentsTable.selectedRowsIds();
1352         if (hashes.length) {
1353             new Request({
1354                 url: "api/v2/torrents/" + cmd,
1355                 method: "post",
1356                 data: {
1357                     hashes: hashes.join("|")
1358                 }
1359             }).send();
1360             updateMainData();
1361         }
1362     };
1364     addClickEvent("about", (e) => {
1365         e.preventDefault();
1366         e.stopPropagation();
1368         const id = "aboutpage";
1369         new MochaUI.Window({
1370             id: id,
1371             icon: "images/qbittorrent-tray.svg",
1372             title: "QBT_TR(About qBittorrent)QBT_TR[CONTEXT=AboutDialog]",
1373             loadMethod: "xhr",
1374             contentURL: new URI("views/about.html").toString(),
1375             require: {
1376                 css: ["css/Tabs.css"]
1377             },
1378             toolbar: true,
1379             toolbarURL: "views/aboutToolbar.html",
1380             padding: 10,
1381             width: loadWindowWidth(id, 550),
1382             height: loadWindowHeight(id, 360),
1383             onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
1384                 saveWindowSize(id);
1385             })
1386         });
1387     });
1389     addClickEvent("logout", (e) => {
1390         e.preventDefault();
1391         e.stopPropagation();
1393         new Request({
1394             url: "api/v2/auth/logout",
1395             method: "post",
1396             onSuccess: function() {
1397                 window.location.reload(true);
1398             }
1399         }).send();
1400     });
1402     addClickEvent("shutdown", (e) => {
1403         e.preventDefault();
1404         e.stopPropagation();
1406         if (confirm("QBT_TR(Are you sure you want to quit qBittorrent?)QBT_TR[CONTEXT=MainWindow]")) {
1407             new Request({
1408                 url: "api/v2/app/shutdown",
1409                 method: "post",
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>`);
1413                     document.close();
1414                     window.stop();
1415                     window.qBittorrent.Client.stop();
1416                 }
1417             }).send();
1418         }
1419     });
1421     // Deactivate menu header links
1422     $$("a.returnFalse").each((el) => {
1423         el.addEventListener("click", (e) => {
1424             e.preventDefault();
1425             e.stopPropagation();
1426         });
1427     });