Enable customizing the save statistics time interval
[qBittorrent.git] / src / webui / www / private / scripts / mocha-init.js
blobfcf613c415c4cab41d1f828d05c9b090b278875d
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
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"))
56                 deepFreeze(value);
57         }
58         Object.freeze(obj);
59     };
61     const baseModalOptions = Object.assign(Object.create(null), {
62         addClass: "modalDialog",
63         collapsible: false,
64         cornerRadius: 5,
65         draggable: true,
66         footerHeight: 20,
67         icon: "images/qbittorrent-tray.svg",
68         loadMethod: "xhr",
69         maximizable: false,
70         method: "post",
71         minimizable: false,
72         padding: {
73             top: 15,
74             right: 10,
75             bottom: 15,
76             left: 5
77         },
78         resizable: true,
79         width: 480
80     });
82     deepFreeze(baseModalOptions);
84     return exports();
85 })();
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);
148     };
150     loadWindowWidth = function(windowId, defaultValue) {
151         return LocalPreferences.get("window_" + windowId + "_width", defaultValue);
152     };
154     loadWindowHeight = function(windowId, defaultValue) {
155         return LocalPreferences.get("window_" + windowId + "_height", defaultValue);
156     };
158     function addClickEvent(el, fn) {
159         ["Link", "Button"].each((item) => {
160             if ($(el + item))
161                 $(el + item).addEventListener("click", fn);
162         });
163     }
165     addClickEvent("download", (e) => {
166         e.preventDefault();
167         e.stopPropagation();
168         showDownloadPage();
169     });
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("|"));
178         new MochaUI.Window({
179             id: id,
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
185             scrollbars: true,
186             maximizable: false,
187             closable: true,
188             paddingVertical: 0,
189             paddingHorizontal: 0,
190             width: loadWindowWidth(id, 500),
191             height: loadWindowHeight(id, 600),
192             onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
193                 saveWindowSize(id);
194             })
195         });
196         updateMainData();
197     };
199     addClickEvent("preferences", (e) => {
200         e.preventDefault();
201         e.stopPropagation();
203         const id = "preferencesPage";
204         new MochaUI.Window({
205             id: id,
206             icon: "images/qbittorrent-tray.svg",
207             title: "QBT_TR(Options)QBT_TR[CONTEXT=OptionsDialog]",
208             loadMethod: "xhr",
209             toolbar: true,
210             contentURL: new URI("views/preferences.html").toString(),
211             require: {
212                 css: ["css/Tabs.css"]
213             },
214             toolbarURL: "views/preferencesToolbar.html",
215             maximizable: false,
216             closable: true,
217             paddingVertical: 0,
218             paddingHorizontal: 0,
219             width: loadWindowWidth(id, 700),
220             height: loadWindowHeight(id, 600),
221             onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
222                 saveWindowSize(id);
223             })
224         });
225     });
227     addClickEvent("upload", (e) => {
228         e.preventDefault();
229         e.stopPropagation();
231         const id = "uploadPage";
232         new MochaUI.Window({
233             id: id,
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
239             scrollbars: true,
240             maximizable: false,
241             paddingVertical: 0,
242             paddingHorizontal: 0,
243             width: loadWindowWidth(id, 500),
244             height: loadWindowHeight(id, 460),
245             onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
246                 saveWindowSize(id);
247             })
248         });
249         updateMainData();
250     });
252     globalUploadLimitFN = function() {
253         new MochaUI.Window({
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(),
259             scrollbars: false,
260             resizable: false,
261             maximizable: false,
262             paddingVertical: 0,
263             paddingHorizontal: 0,
264             width: 424,
265             height: 80
266         });
267     };
269     uploadLimitFN = function() {
270         const hashes = torrentsTable.selectedRowsIds();
271         if (hashes.length) {
272             new MochaUI.Window({
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(),
278                 scrollbars: false,
279                 resizable: false,
280                 maximizable: false,
281                 paddingVertical: 0,
282                 paddingHorizontal: 0,
283                 width: 424,
284                 height: 80
285             });
286         }
287     };
289     shareRatioFN = function() {
290         const hashes = torrentsTable.selectedRowsIds();
291         if (hashes.length) {
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;
302                 // initialize value
303                 if (shareRatio === null)
304                     shareRatio = origValues;
306                 if (origValues !== shareRatio) {
307                     torrentsHaveSameShareRatio = false;
308                     break;
309                 }
310             }
312             // if all torrents have same share ratio, display that share ratio. else use the default
313             const orig = torrentsHaveSameShareRatio ? shareRatio : "";
314             new MochaUI.Window({
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(),
320                 scrollbars: false,
321                 maximizable: false,
322                 paddingVertical: 0,
323                 paddingHorizontal: 0,
324                 width: 424,
325                 height: 200
326             });
327         }
328     };
330     toggleSequentialDownloadFN = function() {
331         const hashes = torrentsTable.selectedRowsIds();
332         if (hashes.length) {
333             new Request({
334                 url: "api/v2/torrents/toggleSequentialDownload",
335                 method: "post",
336                 data: {
337                     hashes: hashes.join("|")
338                 }
339             }).send();
340             updateMainData();
341         }
342     };
344     toggleFirstLastPiecePrioFN = function() {
345         const hashes = torrentsTable.selectedRowsIds();
346         if (hashes.length) {
347             new Request({
348                 url: "api/v2/torrents/toggleFirstLastPiecePrio",
349                 method: "post",
350                 data: {
351                     hashes: hashes.join("|")
352                 }
353             }).send();
354             updateMainData();
355         }
356     };
358     setSuperSeedingFN = function(val) {
359         const hashes = torrentsTable.selectedRowsIds();
360         if (hashes.length) {
361             new Request({
362                 url: "api/v2/torrents/setSuperSeeding",
363                 method: "post",
364                 data: {
365                     value: val,
366                     hashes: hashes.join("|")
367                 }
368             }).send();
369             updateMainData();
370         }
371     };
373     setForceStartFN = function() {
374         const hashes = torrentsTable.selectedRowsIds();
375         if (hashes.length) {
376             new Request({
377                 url: "api/v2/torrents/setForceStart",
378                 method: "post",
379                 data: {
380                     value: "true",
381                     hashes: hashes.join("|")
382                 }
383             }).send();
384             updateMainData();
385         }
386     };
388     globalDownloadLimitFN = function() {
389         new MochaUI.Window({
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(),
395             scrollbars: false,
396             resizable: false,
397             maximizable: false,
398             paddingVertical: 0,
399             paddingHorizontal: 0,
400             width: 424,
401             height: 80
402         });
403     };
405     StatisticsLinkFN = function() {
406         const id = "statisticspage";
407         new MochaUI.Window({
408             id: id,
409             icon: "images/qbittorrent-tray.svg",
410             title: "QBT_TR(Statistics)QBT_TR[CONTEXT=StatsDialog]",
411             loadMethod: "xhr",
412             contentURL: new URI("views/statistics.html").toString(),
413             maximizable: false,
414             padding: 10,
415             width: loadWindowWidth(id, 275),
416             height: loadWindowHeight(id, 370),
417             onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
418                 saveWindowSize(id);
419             })
420         });
421     };
423     downloadLimitFN = function() {
424         const hashes = torrentsTable.selectedRowsIds();
425         if (hashes.length) {
426             new MochaUI.Window({
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(),
432                 scrollbars: false,
433                 resizable: false,
434                 maximizable: false,
435                 paddingVertical: 0,
436                 paddingHorizontal: 0,
437                 width: 424,
438                 height: 80
439             });
440         }
441     };
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) {
447                 new MochaUI.Modal({
448                     ...window.qBittorrent.Dialog.baseModalOptions,
449                     id: "confirmDeletionPage",
450                     title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
451                     data: {
452                         hashes: hashes,
453                         forceDeleteFiles: forceDeleteFiles
454                     },
455                     contentURL: "views/confirmdeletion.html",
456                     onContentLoaded: function(w) {
457                         MochaUI.resizeWindow(w, { centered: true });
458                         MochaUI.centerWindow(w);
459                     },
460                     onCloseComplete: function() {
461                         // make sure overlay is properly hidden upon modal closing
462                         document.getElementById("modalOverlay").style.display = "none";
463                     }
464                 });
465             }
466             else {
467                 new Request({
468                     url: "api/v2/torrents/delete",
469                     method: "post",
470                     data: {
471                         hashes: hashes.join("|"),
472                         deleteFiles: forceDeleteFiles
473                     },
474                     onSuccess: function() {
475                         torrentsTable.deselectAll();
476                         updateMainData();
477                     },
478                     onFailure: function() {
479                         alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
480                     }
481                 }).send();
482             }
483         }
484     };
486     addClickEvent("delete", (e) => {
487         e.preventDefault();
488         e.stopPropagation();
489         deleteFN();
490     });
492     stopFN = function() {
493         const hashes = torrentsTable.selectedRowsIds();
494         if (hashes.length) {
495             new Request({
496                 url: "api/v2/torrents/stop",
497                 method: "post",
498                 data: {
499                     hashes: hashes.join("|")
500                 }
501             }).send();
502             updateMainData();
503         }
504     };
506     startFN = function() {
507         const hashes = torrentsTable.selectedRowsIds();
508         if (hashes.length) {
509             new Request({
510                 url: "api/v2/torrents/start",
511                 method: "post",
512                 data: {
513                     hashes: hashes.join("|")
514                 }
515             }).send();
516             updateMainData();
517         }
518     };
520     autoTorrentManagementFN = function() {
521         const hashes = torrentsTable.selectedRowsIds();
522         if (hashes.length) {
523             let enable = false;
524             hashes.each((hash, index) => {
525                 const row = torrentsTable.rows[hash];
526                 if (!row.full_data.auto_tmm)
527                     enable = true;
528             });
529             new Request({
530                 url: "api/v2/torrents/setAutoManagement",
531                 method: "post",
532                 data: {
533                     hashes: hashes.join("|"),
534                     enable: enable
535                 }
536             }).send();
537             updateMainData();
538         }
539     };
541     recheckFN = function() {
542         const hashes = torrentsTable.selectedRowsIds();
543         if (hashes.length) {
544             new Request({
545                 url: "api/v2/torrents/recheck",
546                 method: "post",
547                 data: {
548                     hashes: hashes.join("|"),
549                 }
550             }).send();
551             updateMainData();
552         }
553     };
555     reannounceFN = function() {
556         const hashes = torrentsTable.selectedRowsIds();
557         if (hashes.length) {
558             new Request({
559                 url: "api/v2/torrents/reannounce",
560                 method: "post",
561                 data: {
562                     hashes: hashes.join("|"),
563                 }
564             }).send();
565             updateMainData();
566         }
567     };
569     setLocationFN = function() {
570         const hashes = torrentsTable.selectedRowsIds();
571         if (hashes.length) {
572             const hash = hashes[0];
573             const row = torrentsTable.rows[hash];
575             new MochaUI.Window({
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(),
581                 scrollbars: false,
582                 resizable: true,
583                 maximizable: false,
584                 paddingVertical: 0,
585                 paddingHorizontal: 0,
586                 width: 400,
587                 height: 130
588             });
589         }
590     };
592     renameFN = function() {
593         const hashes = torrentsTable.selectedRowsIds();
594         if (hashes.length === 1) {
595             const hash = hashes[0];
596             const row = torrentsTable.rows[hash];
597             if (row) {
598                 new MochaUI.Window({
599                     id: "renamePage",
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(),
604                     scrollbars: false,
605                     resizable: true,
606                     maximizable: false,
607                     paddingVertical: 0,
608                     paddingHorizontal: 0,
609                     width: 400,
610                     height: 100
611                 });
612             }
613         }
614     };
616     renameFilesFN = function() {
617         const hashes = torrentsTable.selectedRowsIds();
618         if (hashes.length === 1) {
619             const hash = hashes[0];
620             const row = torrentsTable.rows[hash];
621             if (row) {
622                 new MochaUI.Window({
623                     id: "multiRenamePage",
624                     icon: "images/qbittorrent-tray.svg",
625                     title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TransferListWidget]",
626                     data: { hash: hash, selectedRows: [] },
627                     loadMethod: "xhr",
628                     contentURL: "rename_files.html",
629                     scrollbars: false,
630                     resizable: true,
631                     maximizable: false,
632                     paddingVertical: 0,
633                     paddingHorizontal: 0,
634                     width: 800,
635                     height: 420,
636                     resizeLimit: { "x": [800], "y": [420] }
637                 });
638             }
639         }
640     };
642     torrentNewCategoryFN = function() {
643         const action = "set";
644         const hashes = torrentsTable.selectedRowsIds();
645         if (hashes.length) {
646             new MochaUI.Window({
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(),
652                 scrollbars: false,
653                 resizable: true,
654                 maximizable: false,
655                 paddingVertical: 0,
656                 paddingHorizontal: 0,
657                 width: 400,
658                 height: 150
659             });
660         }
661     };
663     torrentSetCategoryFN = function(categoryHash) {
664         const hashes = torrentsTable.selectedRowsIds();
665         if (hashes.length <= 0)
666             return;
668         const categoryName = category_list.has(categoryHash)
669             ? category_list.get(categoryHash).name
670             : "";
671         new Request({
672             url: "api/v2/torrents/setCategory",
673             method: "post",
674             data: {
675                 hashes: hashes.join("|"),
676                 category: categoryName
677             },
678             onSuccess: function() {
679                 updateMainData();
680             }
681         }).send();
682     };
684     createCategoryFN = function() {
685         const action = "create";
686         new MochaUI.Window({
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(),
692             scrollbars: false,
693             resizable: true,
694             maximizable: false,
695             paddingVertical: 0,
696             paddingHorizontal: 0,
697             width: 400,
698             height: 150
699         });
700     };
702     createSubcategoryFN = function(categoryHash) {
703         const action = "createSubcategory";
704         const categoryName = category_list.get(categoryHash).name + "/";
705         new MochaUI.Window({
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(),
711             scrollbars: false,
712             resizable: true,
713             maximizable: false,
714             paddingVertical: 0,
715             paddingHorizontal: 0,
716             width: 400,
717             height: 150
718         });
719     };
721     editCategoryFN = function(categoryHash) {
722         const action = "edit";
723         const category = category_list.get(categoryHash);
724         new MochaUI.Window({
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(),
730             scrollbars: false,
731             resizable: true,
732             maximizable: false,
733             paddingVertical: 0,
734             paddingHorizontal: 0,
735             width: 400,
736             height: 150
737         });
738     };
740     removeCategoryFN = function(categoryHash) {
741         const categoryName = category_list.get(categoryHash).name;
742         new Request({
743             url: "api/v2/torrents/removeCategories",
744             method: "post",
745             data: {
746                 categories: categoryName
747             },
748             onSuccess: function() {
749                 setCategoryFilter(CATEGORIES_ALL);
750                 updateMainData();
751             }
752         }).send();
753     };
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);
760         });
762         new Request({
763             url: "api/v2/torrents/removeCategories",
764             method: "post",
765             data: {
766                 categories: categories.join("\n")
767             },
768             onSuccess: function() {
769                 setCategoryFilter(CATEGORIES_ALL);
770                 updateMainData();
771             }
772         }).send();
773     };
775     startTorrentsByCategoryFN = function(categoryHash) {
776         const hashes = torrentsTable.getFilteredTorrentsHashes("all", categoryHash, TAGS_ALL, TRACKERS_ALL);
777         if (hashes.length) {
778             new Request({
779                 url: "api/v2/torrents/start",
780                 method: "post",
781                 data: {
782                     hashes: hashes.join("|")
783                 }
784             }).send();
785             updateMainData();
786         }
787     };
789     stopTorrentsByCategoryFN = function(categoryHash) {
790         const hashes = torrentsTable.getFilteredTorrentsHashes("all", categoryHash, TAGS_ALL, TRACKERS_ALL);
791         if (hashes.length) {
792             new Request({
793                 url: "api/v2/torrents/stop",
794                 method: "post",
795                 data: {
796                     hashes: hashes.join("|")
797                 }
798             }).send();
799             updateMainData();
800         }
801     };
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) {
807                 new MochaUI.Modal({
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);
816                     },
817                     onCloseComplete: function() {
818                         // make sure overlay is properly hidden upon modal closing
819                         document.getElementById("modalOverlay").style.display = "none";
820                     }
821                 });
822             }
823             else {
824                 new Request({
825                     url: "api/v2/torrents/delete",
826                     method: "post",
827                     data: {
828                         hashes: hashes.join("|"),
829                         deleteFiles: false,
830                     },
831                     onSuccess: function() {
832                         torrentsTable.deselectAll();
833                         updateMainData();
834                     },
835                     onFailure: function() {
836                         alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
837                     }
838                 }).send();
839             }
840         }
841     };
843     torrentAddTagsFN = function() {
844         const action = "set";
845         const hashes = torrentsTable.selectedRowsIds();
846         if (hashes.length) {
847             new MochaUI.Window({
848                 id: "newTagPage",
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(),
853                 scrollbars: false,
854                 resizable: true,
855                 maximizable: false,
856                 paddingVertical: 0,
857                 paddingHorizontal: 0,
858                 width: 250,
859                 height: 100
860             });
861         }
862     };
864     torrentSetTagsFN = function(tagHash, isSet) {
865         const hashes = torrentsTable.selectedRowsIds();
866         if (hashes.length <= 0)
867             return;
869         const tagName = tagList.has(tagHash) ? tagList.get(tagHash).name : "";
870         new Request({
871             url: (isSet ? "api/v2/torrents/addTags" : "api/v2/torrents/removeTags"),
872             method: "post",
873             data: {
874                 hashes: hashes.join("|"),
875                 tags: tagName,
876             }
877         }).send();
878     };
880     torrentRemoveAllTagsFN = function() {
881         const hashes = torrentsTable.selectedRowsIds();
882         if (hashes.length) {
883             new Request({
884                 url: ("api/v2/torrents/removeTags"),
885                 method: "post",
886                 data: {
887                     hashes: hashes.join("|"),
888                 }
889             }).send();
890         }
891     };
893     createTagFN = function() {
894         const action = "create";
895         new MochaUI.Window({
896             id: "newTagPage",
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(),
901             scrollbars: false,
902             resizable: true,
903             maximizable: false,
904             paddingVertical: 0,
905             paddingHorizontal: 0,
906             width: 250,
907             height: 100
908         });
909         updateMainData();
910     };
912     removeTagFN = function(tagHash) {
913         const tagName = tagList.get(tagHash).name;
914         new Request({
915             url: "api/v2/torrents/deleteTags",
916             method: "post",
917             data: {
918                 tags: tagName
919             }
920         }).send();
921         setTagFilter(TAGS_ALL);
922     };
924     deleteUnusedTagsFN = function() {
925         const tags = [];
926         tagList.forEach((tag, hash) => {
927             if (torrentsTable.getFilteredTorrentsNumber("all", CATEGORIES_ALL, hash, TRACKERS_ALL) === 0)
928                 tags.push(tag.name);
929         });
930         new Request({
931             url: "api/v2/torrents/deleteTags",
932             method: "post",
933             data: {
934                 tags: tags.join(",")
935             }
936         }).send();
937         setTagFilter(TAGS_ALL);
938     };
940     startTorrentsByTagFN = function(tagHash) {
941         const hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, tagHash, TRACKERS_ALL);
942         if (hashes.length) {
943             new Request({
944                 url: "api/v2/torrents/start",
945                 method: "post",
946                 data: {
947                     hashes: hashes.join("|")
948                 }
949             }).send();
950             updateMainData();
951         }
952     };
954     stopTorrentsByTagFN = function(tagHash) {
955         const hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, tagHash, TRACKERS_ALL);
956         if (hashes.length) {
957             new Request({
958                 url: "api/v2/torrents/stop",
959                 method: "post",
960                 data: {
961                     hashes: hashes.join("|")
962                 }
963             }).send();
964             updateMainData();
965         }
966     };
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) {
972                 new MochaUI.Modal({
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);
981                     },
982                     onCloseComplete: function() {
983                         // make sure overlay is properly hidden upon modal closing
984                         document.getElementById("modalOverlay").style.display = "none";
985                     }
986                 });
987             }
988             else {
989                 new Request({
990                     url: "api/v2/torrents/delete",
991                     method: "post",
992                     data: {
993                         hashes: hashes.join("|"),
994                         deleteFiles: false,
995                     },
996                     onSuccess: function() {
997                         torrentsTable.deselectAll();
998                         updateMainData();
999                     },
1000                     onFailure: function() {
1001                         alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
1002                     }
1003                 }).send();
1004             }
1005         }
1006     };
1008     startTorrentsByTrackerFN = function(trackerHash) {
1009         const trackerHashInt = Number.parseInt(trackerHash, 10);
1010         let hashes = [];
1011         switch (trackerHashInt) {
1012             case TRACKERS_ALL:
1013                 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
1014                 break;
1015             case TRACKERS_TRACKERLESS:
1016                 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
1017                 break;
1018             default: {
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);
1023                 }
1024                 hashes = [...uniqueTorrents];
1025                 break;
1026             }
1027         }
1029         if (hashes.length > 0) {
1030             new Request({
1031                 url: "api/v2/torrents/start",
1032                 method: "post",
1033                 data: {
1034                     hashes: hashes.join("|")
1035                 }
1036             }).send();
1037             updateMainData();
1038         }
1039     };
1041     stopTorrentsByTrackerFN = function(trackerHash) {
1042         const trackerHashInt = Number.parseInt(trackerHash, 10);
1043         let hashes = [];
1044         switch (trackerHashInt) {
1045             case TRACKERS_ALL:
1046                 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
1047                 break;
1048             case TRACKERS_TRACKERLESS:
1049                 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
1050                 break;
1051             default: {
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);
1056                 }
1057                 hashes = [...uniqueTorrents];
1058                 break;
1059             }
1060         }
1062         if (hashes.length) {
1063             new Request({
1064                 url: "api/v2/torrents/stop",
1065                 method: "post",
1066                 data: {
1067                     hashes: hashes.join("|")
1068                 }
1069             }).send();
1070             updateMainData();
1071         }
1072     };
1074     deleteTorrentsByTrackerFN = function(trackerHash) {
1075         const trackerHashInt = Number.parseInt(trackerHash, 10);
1076         let hashes = [];
1077         switch (trackerHashInt) {
1078             case TRACKERS_ALL:
1079                 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
1080                 break;
1081             case TRACKERS_TRACKERLESS:
1082                 hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
1083                 break;
1084             default: {
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);
1089                 }
1090                 hashes = [...uniqueTorrents];
1091                 break;
1092             }
1093         }
1095         if (hashes.length > 0) {
1096             if (window.qBittorrent.Cache.preferences.get().confirm_torrent_deletion) {
1097                 new MochaUI.Modal({
1098                     ...window.qBittorrent.Dialog.baseModalOptions,
1099                     id: "confirmDeletionPage",
1100                     title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
1101                     data: {
1102                         hashes: hashes,
1103                         filterList: "tracker"
1104                     },
1105                     contentURL: "views/confirmdeletion.html",
1106                     onContentLoaded: function(w) {
1107                         MochaUI.resizeWindow(w, { centered: true });
1108                         MochaUI.centerWindow(w);
1109                     },
1110                     onCloseComplete: function() {
1111                         // make sure overlay is properly hidden upon modal closing
1112                         document.getElementById("modalOverlay").style.display = "none";
1113                     }
1114                 });
1115             }
1116             else {
1117                 new Request({
1118                     url: "api/v2/torrents/delete",
1119                     method: "post",
1120                     data: {
1121                         hashes: hashes.join("|"),
1122                         deleteFiles: false,
1123                     },
1124                     onSuccess: function() {
1125                         torrentsTable.deselectAll();
1126                         setTrackerFilter(TRACKERS_ALL);
1127                         updateMainData();
1128                     },
1129                     onFailure: function() {
1130                         alert("QBT_TR(Unable to delete torrents.)QBT_TR[CONTEXT=HttpServer]");
1131                     },
1132                 }).send();
1133             }
1134         }
1135     };
1137     copyNameFN = function() {
1138         const selectedRows = torrentsTable.selectedRowsIds();
1139         const names = [];
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);
1145             }
1146         }
1147         return names.join("\n");
1148     };
1150     copyInfohashFN = function(policy) {
1151         const selectedRows = torrentsTable.selectedRowsIds();
1152         const infohashes = [];
1153         if (selectedRows.length > 0) {
1154             const rows = torrentsTable.getFilteredAndSortedRows();
1155             switch (policy) {
1156                 case 1:
1157                     for (const id of selectedRows) {
1158                         const infohash = rows[id].full_data.infohash_v1;
1159                         if (infohash !== "")
1160                             infohashes.push(infohash);
1161                     }
1162                     break;
1163                 case 2:
1164                     for (const id of selectedRows) {
1165                         const infohash = rows[id].full_data.infohash_v2;
1166                         if (infohash !== "")
1167                             infohashes.push(infohash);
1168                     }
1169                     break;
1170             }
1171         }
1172         return infohashes.join("\n");
1173     };
1175     copyMagnetLinkFN = function() {
1176         const selectedRows = torrentsTable.selectedRowsIds();
1177         const magnets = [];
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);
1183             }
1184         }
1185         return magnets.join("\n");
1186     };
1188     copyIdFN = function() {
1189         return torrentsTable.selectedRowsIds().join("\n");
1190     };
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);
1202             }
1203         }
1204         return comments.join("\n---------\n");
1205     };
1207     exportTorrentFN = async function() {
1208         const hashes = torrentsTable.selectedRowsIds();
1209         for (const hash of hashes) {
1210             const row = torrentsTable.rows.get(hash);
1211             if (!row)
1212                 continue;
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");
1220             element.href = url;
1221             element.download = (name + ".torrent");
1222             document.body.appendChild(element);
1223             element.click();
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);
1228         }
1229     };
1231     addClickEvent("stopAll", (e) => {
1232         e.preventDefault();
1233         e.stopPropagation();
1235         if (confirm("QBT_TR(Would you like to stop all torrents?)QBT_TR[CONTEXT=MainWindow]")) {
1236             new Request({
1237                 url: "api/v2/torrents/stop",
1238                 method: "post",
1239                 data: {
1240                     hashes: "all"
1241                 }
1242             }).send();
1243             updateMainData();
1244         }
1245     });
1247     addClickEvent("startAll", (e) => {
1248         e.preventDefault();
1249         e.stopPropagation();
1251         if (confirm("QBT_TR(Would you like to start all torrents?)QBT_TR[CONTEXT=MainWindow]")) {
1252             new Request({
1253                 url: "api/v2/torrents/start",
1254                 method: "post",
1255                 data: {
1256                     hashes: "all"
1257                 }
1258             }).send();
1259             updateMainData();
1260         }
1261     });
1263     ["stop", "start", "recheck"].each((item) => {
1264         addClickEvent(item, (e) => {
1265             e.preventDefault();
1266             e.stopPropagation();
1268             const hashes = torrentsTable.selectedRowsIds();
1269             if (hashes.length) {
1270                 hashes.each((hash, index) => {
1271                     new Request({
1272                         url: "api/v2/torrents/" + item,
1273                         method: "post",
1274                         data: {
1275                             hashes: hash
1276                         }
1277                     }).send();
1278                 });
1279                 updateMainData();
1280             }
1281         });
1282     });
1284     ["decreasePrio", "increasePrio", "topPrio", "bottomPrio"].each((item) => {
1285         addClickEvent(item, (e) => {
1286             e.preventDefault();
1287             e.stopPropagation();
1288             setQueuePositionFN(item);
1289         });
1290     });
1292     setQueuePositionFN = function(cmd) {
1293         const hashes = torrentsTable.selectedRowsIds();
1294         if (hashes.length) {
1295             new Request({
1296                 url: "api/v2/torrents/" + cmd,
1297                 method: "post",
1298                 data: {
1299                     hashes: hashes.join("|")
1300                 }
1301             }).send();
1302             updateMainData();
1303         }
1304     };
1306     addClickEvent("about", (e) => {
1307         e.preventDefault();
1308         e.stopPropagation();
1310         const id = "aboutpage";
1311         new MochaUI.Window({
1312             id: id,
1313             icon: "images/qbittorrent-tray.svg",
1314             title: "QBT_TR(About qBittorrent)QBT_TR[CONTEXT=AboutDialog]",
1315             loadMethod: "xhr",
1316             contentURL: new URI("views/about.html").toString(),
1317             require: {
1318                 css: ["css/Tabs.css"]
1319             },
1320             toolbar: true,
1321             toolbarURL: "views/aboutToolbar.html",
1322             padding: 10,
1323             width: loadWindowWidth(id, 550),
1324             height: loadWindowHeight(id, 360),
1325             onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
1326                 saveWindowSize(id);
1327             })
1328         });
1329     });
1331     addClickEvent("logout", (e) => {
1332         e.preventDefault();
1333         e.stopPropagation();
1335         new Request({
1336             url: "api/v2/auth/logout",
1337             method: "post",
1338             onSuccess: function() {
1339                 window.location.reload(true);
1340             }
1341         }).send();
1342     });
1344     addClickEvent("shutdown", (e) => {
1345         e.preventDefault();
1346         e.stopPropagation();
1348         if (confirm("QBT_TR(Are you sure you want to quit qBittorrent?)QBT_TR[CONTEXT=MainWindow]")) {
1349             new Request({
1350                 url: "api/v2/app/shutdown",
1351                 method: "post",
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>`);
1355                     document.close();
1356                     window.stop();
1357                     window.qBittorrent.Client.stop();
1358                 }
1359             }).send();
1360         }
1361     });
1363     // Deactivate menu header links
1364     $$("a.returnFalse").each((el) => {
1365         el.addEventListener("click", (e) => {
1366             e.preventDefault();
1367             e.stopPropagation();
1368         });
1369     });