WebUI: Always create generic filter items
[qBittorrent.git] / src / webui / www / private / scripts / client.js
blobc9d8e7032e0ba2396e23adbc4281d36a8d14c419
1 /*
2  * MIT License
3  * Copyright (C) 2024  Mike Tzou (Chocobo1)
4  * Copyright (c) 2008 Ishan Arora <ishan@qbittorrent.org>,
5  * Christophe Dumez <chris@qbittorrent.org>
6  *
7  * Permission is hereby granted, free of charge, to any person obtaining a copy
8  * of this software and associated documentation files (the "Software"), to deal
9  * in the Software without restriction, including without limitation the rights
10  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11  * copies of the Software, and to permit persons to whom the Software is
12  * furnished to do so, subject to the following conditions:
13  *
14  * The above copyright notice and this permission notice shall be included in
15  * all copies or substantial portions of the Software.
16  *
17  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23  * THE SOFTWARE.
24  */
26 "use strict";
28 if (window.qBittorrent === undefined)
29     window.qBittorrent = {};
31 window.qBittorrent.Client = (() => {
32     const exports = () => {
33         return {
34             closeWindows: closeWindows,
35             genHash: genHash,
36             getSyncMainDataInterval: getSyncMainDataInterval,
37             isStopped: isStopped,
38             stop: stop,
39             mainTitle: mainTitle,
40             showSearchEngine: showSearchEngine,
41             showRssReader: showRssReader,
42             showLogViewer: showLogViewer,
43             isShowSearchEngine: isShowSearchEngine,
44             isShowRssReader: isShowRssReader,
45             isShowLogViewer: isShowLogViewer
46         };
47     };
49     const closeWindows = function() {
50         MochaUI.closeAll();
51     };
53     const genHash = function(string) {
54         // origins:
55         // https://stackoverflow.com/a/8831937
56         // https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0
57         let hash = 0;
58         for (let i = 0; i < string.length; ++i)
59             hash = ((Math.imul(hash, 31) + string.charCodeAt(i)) | 0);
60         return hash;
61     };
63     const getSyncMainDataInterval = function() {
64         return customSyncMainDataInterval ? customSyncMainDataInterval : serverSyncMainDataInterval;
65     };
67     let stopped = false;
68     const isStopped = () => {
69         return stopped;
70     };
72     const stop = () => {
73         stopped = true;
74     };
76     const mainTitle = () => {
77         const emDash = "\u2014";
78         const qbtVersion = window.qBittorrent.Cache.qbtVersion.get();
79         const suffix = window.qBittorrent.Cache.preferences.get()["app_instance_name"] || "";
80         const title = `qBittorrent ${qbtVersion} QBT_TR(WebUI)QBT_TR[CONTEXT=OptionsDialog]`
81             + ((suffix.length > 0) ? ` ${emDash} ${suffix}` : "");
82         return title;
83     };
85     let showingSearchEngine = false;
86     let showingRssReader = false;
87     let showingLogViewer = false;
89     const showSearchEngine = function(bool) {
90         showingSearchEngine = bool;
91     };
92     const showRssReader = function(bool) {
93         showingRssReader = bool;
94     };
95     const showLogViewer = function(bool) {
96         showingLogViewer = bool;
97     };
98     const isShowSearchEngine = function() {
99         return showingSearchEngine;
100     };
101     const isShowRssReader = function() {
102         return showingRssReader;
103     };
104     const isShowLogViewer = function() {
105         return showingLogViewer;
106     };
108     return exports();
109 })();
110 Object.freeze(window.qBittorrent.Client);
112 // TODO: move global functions/variables into some namespace/scope
114 this.torrentsTable = new window.qBittorrent.DynamicTable.TorrentsTable();
116 let updatePropertiesPanel = function() {};
118 this.updateMainData = function() {};
119 let alternativeSpeedLimits = false;
120 let queueing_enabled = true;
121 let serverSyncMainDataInterval = 1500;
122 let customSyncMainDataInterval = null;
123 let useSubcategories = true;
125 /* Categories filter */
126 const CATEGORIES_ALL = 1;
127 const CATEGORIES_UNCATEGORIZED = 2;
129 const category_list = new Map();
131 let selected_category = Number(LocalPreferences.get("selected_category", CATEGORIES_ALL));
132 let setCategoryFilter = function() {};
134 /* Tags filter */
135 const TAGS_ALL = 1;
136 const TAGS_UNTAGGED = 2;
138 const tagList = new Map();
140 let selectedTag = Number(LocalPreferences.get("selected_tag", TAGS_ALL));
141 let setTagFilter = function() {};
143 /* Trackers filter */
144 const TRACKERS_ALL = 1;
145 const TRACKERS_TRACKERLESS = 2;
147 /** @type Map<number, {host: string, trackerTorrentMap: Map<string, string[]>}> **/
148 const trackerList = new Map();
150 let selectedTracker = LocalPreferences.get("selected_tracker", TRACKERS_ALL);
151 let setTrackerFilter = function() {};
153 /* All filters */
154 let selected_filter = LocalPreferences.get("selected_filter", "all");
155 let setFilter = function() {};
156 let toggleFilterDisplay = function() {};
158 window.addEventListener("DOMContentLoaded", () => {
159     let isSearchPanelLoaded = false;
160     let isLogPanelLoaded = false;
162     const saveColumnSizes = function() {
163         const filters_width = $("Filters").getSize().x;
164         LocalPreferences.set("filters_width", filters_width);
165         const properties_height_rel = $("propertiesPanel").getSize().y / Window.getSize().y;
166         LocalPreferences.set("properties_height_rel", properties_height_rel);
167     };
169     window.addEvent("resize", () => {
170         // only save sizes if the columns are visible
171         if (!$("mainColumn").hasClass("invisible"))
172             saveColumnSizes.delay(200); // Resizing might takes some time.
173     });
175     /* MochaUI.Desktop = new MochaUI.Desktop();
176     MochaUI.Desktop.desktop.setStyles({
177         'background': '#fff',
178         'visibility': 'visible'
179     });*/
180     MochaUI.Desktop.initialize();
182     const buildTransfersTab = function() {
183         const filt_w = Number(LocalPreferences.get("filters_width", 120));
184         new MochaUI.Column({
185             id: "filtersColumn",
186             placement: "left",
187             onResize: saveColumnSizes,
188             width: filt_w,
189             resizeLimit: [1, 300]
190         });
191         new MochaUI.Column({
192             id: "mainColumn",
193             placement: "main"
194         });
195     };
197     const buildSearchTab = function() {
198         new MochaUI.Column({
199             id: "searchTabColumn",
200             placement: "main",
201             width: null
202         });
204         // start off hidden
205         $("searchTabColumn").addClass("invisible");
206     };
208     const buildRssTab = function() {
209         new MochaUI.Column({
210             id: "rssTabColumn",
211             placement: "main",
212             width: null
213         });
215         // start off hidden
216         $("rssTabColumn").addClass("invisible");
217     };
219     const buildLogTab = function() {
220         new MochaUI.Column({
221             id: "logTabColumn",
222             placement: "main",
223             width: null
224         });
226         // start off hidden
227         $("logTabColumn").addClass("invisible");
228     };
230     buildTransfersTab();
231     buildSearchTab();
232     buildRssTab();
233     buildLogTab();
234     MochaUI.initializeTabs("mainWindowTabsList");
236     setCategoryFilter = function(hash) {
237         selected_category = hash;
238         LocalPreferences.set("selected_category", selected_category);
239         highlightSelectedCategory();
240         if (typeof torrentsTable.tableBody !== "undefined")
241             updateMainData();
242     };
244     setTagFilter = function(hash) {
245         selectedTag = hash;
246         LocalPreferences.set("selected_tag", selectedTag);
247         highlightSelectedTag();
248         if (torrentsTable.tableBody !== undefined)
249             updateMainData();
250     };
252     setTrackerFilter = function(hash) {
253         selectedTracker = hash.toString();
254         LocalPreferences.set("selected_tracker", selectedTracker);
255         highlightSelectedTracker();
256         if (torrentsTable.tableBody !== undefined)
257             updateMainData();
258     };
260     setFilter = function(f) {
261         // Visually Select the right filter
262         $("all_filter").removeClass("selectedFilter");
263         $("downloading_filter").removeClass("selectedFilter");
264         $("seeding_filter").removeClass("selectedFilter");
265         $("completed_filter").removeClass("selectedFilter");
266         $("stopped_filter").removeClass("selectedFilter");
267         $("running_filter").removeClass("selectedFilter");
268         $("active_filter").removeClass("selectedFilter");
269         $("inactive_filter").removeClass("selectedFilter");
270         $("stalled_filter").removeClass("selectedFilter");
271         $("stalled_uploading_filter").removeClass("selectedFilter");
272         $("stalled_downloading_filter").removeClass("selectedFilter");
273         $("checking_filter").removeClass("selectedFilter");
274         $("moving_filter").removeClass("selectedFilter");
275         $("errored_filter").removeClass("selectedFilter");
276         $(f + "_filter").addClass("selectedFilter");
277         selected_filter = f;
278         LocalPreferences.set("selected_filter", f);
279         // Reload torrents
280         if (typeof torrentsTable.tableBody !== "undefined")
281             updateMainData();
282     };
284     toggleFilterDisplay = function(filter) {
285         const element = filter + "FilterList";
286         LocalPreferences.set("filter_" + filter + "_collapsed", !$(element).hasClass("invisible"));
287         $(element).toggleClass("invisible");
288         const parent = $(element).getParent(".filterWrapper");
289         const toggleIcon = $(parent).getChildren(".filterTitle img");
290         if (toggleIcon)
291             toggleIcon[0].toggleClass("rotate");
292     };
294     new MochaUI.Panel({
295         id: "Filters",
296         title: "Panel",
297         header: false,
298         padding: {
299             top: 0,
300             right: 0,
301             bottom: 0,
302             left: 0
303         },
304         loadMethod: "xhr",
305         contentURL: "views/filters.html",
306         onContentLoaded: function() {
307             setFilter(selected_filter);
308         },
309         column: "filtersColumn",
310         height: 300
311     });
312     initializeWindows();
314     // Show Top Toolbar is enabled by default
315     let showTopToolbar = LocalPreferences.get("show_top_toolbar", "true") === "true";
316     if (!showTopToolbar) {
317         $("showTopToolbarLink").firstChild.style.opacity = "0";
318         $("mochaToolbar").addClass("invisible");
319     }
321     // Show Status Bar is enabled by default
322     let showStatusBar = LocalPreferences.get("show_status_bar", "true") === "true";
323     if (!showStatusBar) {
324         $("showStatusBarLink").firstChild.style.opacity = "0";
325         $("desktopFooterWrapper").addClass("invisible");
326     }
328     // Show Filters Sidebar is enabled by default
329     let showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
330     if (!showFiltersSidebar) {
331         $("showFiltersSidebarLink").firstChild.style.opacity = "0";
332         $("filtersColumn").addClass("invisible");
333         $("filtersColumn_handle").addClass("invisible");
334     }
336     let speedInTitle = LocalPreferences.get("speed_in_browser_title_bar") === "true";
337     if (!speedInTitle)
338         $("speedInBrowserTitleBarLink").firstChild.style.opacity = "0";
340     // After showing/hiding the toolbar + status bar
341     window.qBittorrent.Client.showSearchEngine(LocalPreferences.get("show_search_engine") !== "false");
342     window.qBittorrent.Client.showRssReader(LocalPreferences.get("show_rss_reader") !== "false");
343     window.qBittorrent.Client.showLogViewer(LocalPreferences.get("show_log_viewer") === "true");
345     // After Show Top Toolbar
346     MochaUI.Desktop.setDesktopSize();
348     let syncMainDataLastResponseId = 0;
349     const serverState = {};
351     const removeTorrentFromCategoryList = function(hash) {
352         if (!hash)
353             return false;
355         let removed = false;
356         category_list.forEach((category) => {
357             const deleteResult = category.torrents.delete(hash);
358             removed ||= deleteResult;
359         });
361         return removed;
362     };
364     const addTorrentToCategoryList = function(torrent) {
365         const category = torrent["category"];
366         if (typeof category === "undefined")
367             return false;
369         const hash = torrent["hash"];
370         if (category.length === 0) { // Empty category
371             removeTorrentFromCategoryList(hash);
372             return true;
373         }
375         const categoryHash = window.qBittorrent.Client.genHash(category);
376         if (!category_list.has(categoryHash)) { // This should not happen
377             category_list.set(categoryHash, {
378                 name: category,
379                 torrents: new Set()
380             });
381         }
383         const torrents = category_list.get(categoryHash).torrents;
384         if (!torrents.has(hash)) {
385             removeTorrentFromCategoryList(hash);
386             torrents.add(hash);
387             return true;
388         }
389         return false;
390     };
392     const removeTorrentFromTagList = function(hash) {
393         if (!hash)
394             return false;
396         let removed = false;
397         tagList.forEach((tag) => {
398             const deleteResult = tag.torrents.delete(hash);
399             removed ||= deleteResult;
400         });
402         return removed;
403     };
405     const addTorrentToTagList = function(torrent) {
406         if (torrent["tags"] === undefined) // Tags haven't changed
407             return false;
409         const hash = torrent["hash"];
410         removeTorrentFromTagList(hash);
412         if (torrent["tags"].length === 0) // No tags
413             return true;
415         const tags = torrent["tags"].split(",");
416         let added = false;
417         for (let i = 0; i < tags.length; ++i) {
418             const tagHash = window.qBittorrent.Client.genHash(tags[i].trim());
419             if (!tagList.has(tagHash)) { // This should not happen
420                 tagList.set(tagHash, {
421                     name: tags,
422                     torrents: new Set()
423                 });
424             }
426             const torrents = tagList.get(tagHash).torrents;
427             if (!torrents.has(hash)) {
428                 torrents.add(hash);
429                 added = true;
430             }
431         }
432         return added;
433     };
435     const updateFilter = function(filter, filterTitle) {
436         $(filter + "_filter").firstChild.childNodes[1].nodeValue = filterTitle.replace("%1", torrentsTable.getFilteredTorrentsNumber(filter, CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL));
437     };
439     const updateFiltersList = function() {
440         updateFilter("all", "QBT_TR(All (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
441         updateFilter("downloading", "QBT_TR(Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
442         updateFilter("seeding", "QBT_TR(Seeding (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
443         updateFilter("completed", "QBT_TR(Completed (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
444         updateFilter("running", "QBT_TR(Running (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
445         updateFilter("stopped", "QBT_TR(Stopped (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
446         updateFilter("active", "QBT_TR(Active (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
447         updateFilter("inactive", "QBT_TR(Inactive (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
448         updateFilter("stalled", "QBT_TR(Stalled (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
449         updateFilter("stalled_uploading", "QBT_TR(Stalled Uploading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
450         updateFilter("stalled_downloading", "QBT_TR(Stalled Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
451         updateFilter("checking", "QBT_TR(Checking (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
452         updateFilter("moving", "QBT_TR(Moving (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
453         updateFilter("errored", "QBT_TR(Errored (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
454     };
456     const updateCategoryList = function() {
457         const categoryList = $("categoryFilterList");
458         if (!categoryList)
459             return;
460         categoryList.getChildren().each(c => c.destroy());
462         const create_link = function(hash, text, count) {
463             let display_name = text;
464             let margin_left = 0;
465             if (useSubcategories) {
466                 const category_path = text.split("/");
467                 display_name = category_path[category_path.length - 1];
468                 margin_left = (category_path.length - 1) * 20;
469             }
471             const html = `<span class="link" href="#" style="margin-left: ${margin_left}px;" onclick="setCategoryFilter(${hash}); return false;">`
472                 + '<img src="images/view-categories.svg"/>'
473                 + window.qBittorrent.Misc.escapeHtml(display_name) + " (" + count + ")" + "</span>";
474             const el = new Element("li", {
475                 id: hash,
476                 html: html
477             });
478             window.qBittorrent.Filters.categoriesFilterContextMenu.addTarget(el);
479             return el;
480         };
482         const all = torrentsTable.getRowIds().length;
483         let uncategorized = 0;
484         for (const key in torrentsTable.rows) {
485             if (!Object.hasOwn(torrentsTable.rows, key))
486                 continue;
488             const row = torrentsTable.rows[key];
489             if (row["full_data"].category.length === 0)
490                 uncategorized += 1;
491         }
492         categoryList.appendChild(create_link(CATEGORIES_ALL, "QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]", all));
493         categoryList.appendChild(create_link(CATEGORIES_UNCATEGORIZED, "QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]", uncategorized));
495         const sortedCategories = [];
496         category_list.forEach((category, hash) => sortedCategories.push({
497             categoryName: category.name,
498             categoryHash: hash,
499             categoryCount: category.torrents.size
500         }));
501         sortedCategories.sort((left, right) => {
502             const leftSegments = left.categoryName.split("/");
503             const rightSegments = right.categoryName.split("/");
505             for (let i = 0, iMax = Math.min(leftSegments.length, rightSegments.length); i < iMax; ++i) {
506                 const compareResult = window.qBittorrent.Misc.naturalSortCollator.compare(
507                     leftSegments[i], rightSegments[i]);
508                 if (compareResult !== 0)
509                     return compareResult;
510             }
512             return leftSegments.length - rightSegments.length;
513         });
515         for (let i = 0; i < sortedCategories.length; ++i) {
516             const { categoryName, categoryHash } = sortedCategories[i];
517             let { categoryCount } = sortedCategories[i];
519             if (useSubcategories) {
520                 for (let j = (i + 1);
521                     ((j < sortedCategories.length) && sortedCategories[j].categoryName.startsWith(categoryName + "/")); ++j)
522                     categoryCount += sortedCategories[j].categoryCount;
523             }
525             categoryList.appendChild(create_link(categoryHash, categoryName, categoryCount));
526         }
528         highlightSelectedCategory();
529     };
531     const highlightSelectedCategory = function() {
532         const categoryList = $("categoryFilterList");
533         if (!categoryList)
534             return;
535         const children = categoryList.childNodes;
536         for (let i = 0; i < children.length; ++i) {
537             if (Number(children[i].id) === selected_category)
538                 children[i].className = "selectedFilter";
539             else
540                 children[i].className = "";
541         }
542     };
544     const updateTagList = function() {
545         const tagFilterList = $("tagFilterList");
546         if (tagFilterList === null)
547             return;
549         tagFilterList.getChildren().each(c => c.destroy());
551         const createLink = function(hash, text, count) {
552             const html = `<span class="link" href="#" onclick="setTagFilter(${hash}); return false;">`
553                 + '<img src="images/tags.svg"/>'
554                 + window.qBittorrent.Misc.escapeHtml(text) + " (" + count + ")" + "</span>";
555             const el = new Element("li", {
556                 id: hash,
557                 html: html
558             });
559             window.qBittorrent.Filters.tagsFilterContextMenu.addTarget(el);
560             return el;
561         };
563         const torrentsCount = torrentsTable.getRowIds().length;
564         let untagged = 0;
565         for (const key in torrentsTable.rows) {
566             if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]["full_data"].tags.length === 0))
567                 untagged += 1;
568         }
569         tagFilterList.appendChild(createLink(TAGS_ALL, "QBT_TR(All)QBT_TR[CONTEXT=TagFilterModel]", torrentsCount));
570         tagFilterList.appendChild(createLink(TAGS_UNTAGGED, "QBT_TR(Untagged)QBT_TR[CONTEXT=TagFilterModel]", untagged));
572         const sortedTags = [];
573         tagList.forEach((tag, hash) => sortedTags.push({
574             tagName: tag.name,
575             tagHash: hash,
576             tagSize: tag.torrents.size
577         }));
578         sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName));
580         for (const { tagName, tagHash, tagSize } of sortedTags)
581             tagFilterList.appendChild(createLink(tagHash, tagName, tagSize));
583         highlightSelectedTag();
584     };
586     const highlightSelectedTag = function() {
587         const tagFilterList = $("tagFilterList");
588         if (!tagFilterList)
589             return;
591         const children = tagFilterList.childNodes;
592         for (let i = 0; i < children.length; ++i)
593             children[i].className = (Number(children[i].id) === selectedTag) ? "selectedFilter" : "";
594     };
596     // getHost emulate the GUI version `QString getHost(const QString &url)`
597     const getHost = function(url) {
598         // We want the hostname.
599         // If failed to parse the domain, original input should be returned
601         if (!/^(?:https?|udp):/i.test(url))
602             return url;
604         try {
605             // hack: URL can not get hostname from udp protocol
606             const parsedUrl = new URL(url.replace(/^udp:/i, "https:"));
607             // host: "example.com:8443"
608             // hostname: "example.com"
609             const host = parsedUrl.hostname;
610             if (!host)
611                 return url;
613             return host;
614         }
615         catch (error) {
616             return url;
617         }
618     };
620     const updateTrackerList = function() {
621         const trackerFilterList = $("trackerFilterList");
622         if (trackerFilterList === null)
623             return;
625         trackerFilterList.getChildren().each(c => c.destroy());
627         const createLink = function(hash, text, count) {
628             const html = '<span class="link" href="#" onclick="setTrackerFilter(' + hash + ');return false;">'
629                 + '<img src="images/trackers.svg"/>'
630                 + window.qBittorrent.Misc.escapeHtml(text.replace("%1", count)) + "</span>";
631             const el = new Element("li", {
632                 id: hash,
633                 html: html
634             });
635             window.qBittorrent.Filters.trackersFilterContextMenu.addTarget(el);
636             return el;
637         };
639         const torrentsCount = torrentsTable.getRowIds().length;
640         trackerFilterList.appendChild(createLink(TRACKERS_ALL, "QBT_TR(All (%1))QBT_TR[CONTEXT=TrackerFiltersList]", torrentsCount));
641         let trackerlessTorrentsCount = 0;
642         for (const key in torrentsTable.rows) {
643             if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]["full_data"].trackers_count === 0))
644                 trackerlessTorrentsCount += 1;
645         }
646         trackerFilterList.appendChild(createLink(TRACKERS_TRACKERLESS, "QBT_TR(Trackerless (%1))QBT_TR[CONTEXT=TrackerFiltersList]", trackerlessTorrentsCount));
648         // Sort trackers by hostname
649         const sortedList = [];
650         trackerList.forEach(({ host, trackerTorrentMap }, hash) => {
651             const uniqueTorrents = new Set();
652             for (const torrents of trackerTorrentMap.values()) {
653                 for (const torrent of torrents)
654                     uniqueTorrents.add(torrent);
655             }
657             sortedList.push({
658                 trackerHost: host,
659                 trackerHash: hash,
660                 trackerCount: uniqueTorrents.size,
661             });
662         });
663         sortedList.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.trackerHost, right.trackerHost));
664         for (const { trackerHost, trackerHash, trackerCount } of sortedList)
665             trackerFilterList.appendChild(createLink(trackerHash, (trackerHost + " (%1)"), trackerCount));
667         highlightSelectedTracker();
668     };
670     const highlightSelectedTracker = function() {
671         const trackerFilterList = $("trackerFilterList");
672         if (!trackerFilterList)
673             return;
675         const children = trackerFilterList.childNodes;
676         for (const child of children)
677             child.className = (child.id === selectedTracker) ? "selectedFilter" : "";
678     };
680     const setupCopyEventHandler = (function() {
681         let clipboardEvent;
683         return () => {
684             if (clipboardEvent)
685                 clipboardEvent.destroy();
687             clipboardEvent = new ClipboardJS(".copyToClipboard", {
688                 text: function(trigger) {
689                     switch (trigger.id) {
690                         case "copyName":
691                             return copyNameFN();
692                         case "copyInfohash1":
693                             return copyInfohashFN(1);
694                         case "copyInfohash2":
695                             return copyInfohashFN(2);
696                         case "copyMagnetLink":
697                             return copyMagnetLinkFN();
698                         case "copyID":
699                             return copyIdFN();
700                         case "copyComment":
701                             return copyCommentFN();
702                         default:
703                             return "";
704                     }
705                 }
706             });
707         };
708     })();
710     let syncMainDataTimeoutID;
711     let syncRequestInProgress = false;
712     const syncMainData = function() {
713         const url = new URI("api/v2/sync/maindata");
714         url.setData("rid", syncMainDataLastResponseId);
715         const request = new Request.JSON({
716             url: url,
717             noCache: true,
718             method: "get",
719             onFailure: function() {
720                 const errorDiv = $("error_div");
721                 if (errorDiv)
722                     errorDiv.set("html", "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]");
723                 syncRequestInProgress = false;
724                 syncData(2000);
725             },
726             onSuccess: function(response) {
727                 $("error_div").set("html", "");
728                 if (response) {
729                     clearTimeout(torrentsFilterInputTimer);
730                     torrentsFilterInputTimer = -1;
732                     let torrentsTableSelectedRows;
733                     let update_categories = false;
734                     let updateTags = false;
735                     let updateTrackers = false;
736                     const full_update = (response["full_update"] === true);
737                     if (full_update) {
738                         torrentsTableSelectedRows = torrentsTable.selectedRowsIds();
739                         update_categories = true;
740                         updateTags = true;
741                         updateTrackers = true;
742                         torrentsTable.clear();
743                         category_list.clear();
744                         tagList.clear();
745                         trackerList.clear();
746                     }
747                     if (response["rid"])
748                         syncMainDataLastResponseId = response["rid"];
749                     if (response["categories"]) {
750                         for (const key in response["categories"]) {
751                             if (!Object.hasOwn(response["categories"], key))
752                                 continue;
754                             const responseCategory = response["categories"][key];
755                             const categoryHash = window.qBittorrent.Client.genHash(key);
756                             const category = category_list.get(categoryHash);
757                             if (category !== undefined) {
758                                 // only the save path can change for existing categories
759                                 category.savePath = responseCategory.savePath;
760                             }
761                             else {
762                                 category_list.set(categoryHash, {
763                                     name: responseCategory.name,
764                                     savePath: responseCategory.savePath,
765                                     torrents: new Set()
766                                 });
767                             }
768                         }
769                         update_categories = true;
770                     }
771                     if (response["categories_removed"]) {
772                         response["categories_removed"].each((category) => {
773                             const categoryHash = window.qBittorrent.Client.genHash(category);
774                             category_list.delete(categoryHash);
775                         });
776                         update_categories = true;
777                     }
778                     if (response["tags"]) {
779                         for (const tag of response["tags"]) {
780                             const tagHash = window.qBittorrent.Client.genHash(tag);
781                             if (!tagList.has(tagHash)) {
782                                 tagList.set(tagHash, {
783                                     name: tag,
784                                     torrents: new Set()
785                                 });
786                             }
787                         }
788                         updateTags = true;
789                     }
790                     if (response["tags_removed"]) {
791                         for (let i = 0; i < response["tags_removed"].length; ++i) {
792                             const tagHash = window.qBittorrent.Client.genHash(response["tags_removed"][i]);
793                             tagList.delete(tagHash);
794                         }
795                         updateTags = true;
796                     }
797                     if (response["trackers"]) {
798                         for (const [tracker, torrents] of Object.entries(response["trackers"])) {
799                             const host = getHost(tracker);
800                             const hash = window.qBittorrent.Client.genHash(host);
802                             let trackerListItem = trackerList.get(hash);
803                             if (trackerListItem === undefined) {
804                                 trackerListItem = { host: host, trackerTorrentMap: new Map() };
805                                 trackerList.set(hash, trackerListItem);
806                             }
808                             trackerListItem.trackerTorrentMap.set(tracker, [...torrents]);
809                         }
810                         updateTrackers = true;
811                     }
812                     if (response["trackers_removed"]) {
813                         for (let i = 0; i < response["trackers_removed"].length; ++i) {
814                             const tracker = response["trackers_removed"][i];
815                             const hash = window.qBittorrent.Client.genHash(getHost(tracker));
816                             const trackerListEntry = trackerList.get(hash);
817                             if (trackerListEntry)
818                                 trackerListEntry.trackerTorrentMap.delete(tracker);
819                         }
820                         updateTrackers = true;
821                     }
822                     if (response["torrents"]) {
823                         let updateTorrentList = false;
824                         for (const key in response["torrents"]) {
825                             if (!Object.hasOwn(response["torrents"], key))
826                                 continue;
828                             response["torrents"][key]["hash"] = key;
829                             response["torrents"][key]["rowId"] = key;
830                             if (response["torrents"][key]["state"])
831                                 response["torrents"][key]["status"] = response["torrents"][key]["state"];
832                             torrentsTable.updateRowData(response["torrents"][key]);
833                             if (addTorrentToCategoryList(response["torrents"][key]))
834                                 update_categories = true;
835                             if (addTorrentToTagList(response["torrents"][key]))
836                                 updateTags = true;
837                             if (response["torrents"][key]["name"])
838                                 updateTorrentList = true;
839                         }
841                         if (updateTorrentList)
842                             setupCopyEventHandler();
843                     }
844                     if (response["torrents_removed"]) {
845                         response["torrents_removed"].each((hash) => {
846                             torrentsTable.removeRow(hash);
847                             removeTorrentFromCategoryList(hash);
848                             update_categories = true; // Always to update All category
849                             removeTorrentFromTagList(hash);
850                             updateTags = true; // Always to update All tag
851                         });
852                     }
853                     torrentsTable.updateTable(full_update);
854                     torrentsTable.altRow();
855                     if (response["server_state"]) {
856                         const tmp = response["server_state"];
857                         for (const k in tmp) {
858                             if (!Object.hasOwn(tmp, k))
859                                 continue;
860                             serverState[k] = tmp[k];
861                         }
862                         processServerState();
863                     }
864                     updateFiltersList();
865                     if (update_categories) {
866                         updateCategoryList();
867                         window.qBittorrent.TransferList.contextMenu.updateCategoriesSubMenu(category_list);
868                     }
869                     if (updateTags) {
870                         updateTagList();
871                         window.qBittorrent.TransferList.contextMenu.updateTagsSubMenu(tagList);
872                     }
873                     if (updateTrackers)
874                         updateTrackerList();
876                     if (full_update)
877                         // re-select previously selected rows
878                         torrentsTable.reselectRows(torrentsTableSelectedRows);
879                 }
880                 syncRequestInProgress = false;
881                 syncData(window.qBittorrent.Client.getSyncMainDataInterval());
882             }
883         });
884         syncRequestInProgress = true;
885         request.send();
886     };
888     updateMainData = function() {
889         torrentsTable.updateTable();
890         syncData(100);
891     };
893     const syncData = function(delay) {
894         if (syncRequestInProgress)
895             return;
897         clearTimeout(syncMainDataTimeoutID);
899         if (window.qBittorrent.Client.isStopped())
900             return;
902         syncMainDataTimeoutID = syncMainData.delay(delay);
903     };
905     const processServerState = function() {
906         let transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true);
907         if (serverState.dl_rate_limit > 0)
908             transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_rate_limit, true) + "]";
909         transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_data, false) + ")";
910         $("DlInfos").set("html", transfer_info);
911         transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true);
912         if (serverState.up_rate_limit > 0)
913             transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.up_rate_limit, true) + "]";
914         transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.up_info_data, false) + ")";
915         $("UpInfos").set("html", transfer_info);
917         document.title = (speedInTitle
918                 ? (`QBT_TR([D: %1, U: %2])QBT_TR[CONTEXT=MainWindow] `
919                     .replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true))
920                     .replace("%2", window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true)))
921                 : "")
922             + window.qBittorrent.Client.mainTitle();
924         $("freeSpaceOnDisk").set("html", "QBT_TR(Free space: %1)QBT_TR[CONTEXT=HttpServer]".replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.free_space_on_disk)));
925         $("DHTNodes").set("html", "QBT_TR(DHT: %1 nodes)QBT_TR[CONTEXT=StatusBar]".replace("%1", serverState.dht_nodes));
927         // Statistics dialog
928         if (document.getElementById("statisticsContent")) {
929             $("AlltimeDL").set("html", window.qBittorrent.Misc.friendlyUnit(serverState.alltime_dl, false));
930             $("AlltimeUL").set("html", window.qBittorrent.Misc.friendlyUnit(serverState.alltime_ul, false));
931             $("TotalWastedSession").set("html", window.qBittorrent.Misc.friendlyUnit(serverState.total_wasted_session, false));
932             $("GlobalRatio").set("html", serverState.global_ratio);
933             $("TotalPeerConnections").set("html", serverState.total_peer_connections);
934             $("ReadCacheHits").set("html", serverState.read_cache_hits + "%");
935             $("TotalBuffersSize").set("html", window.qBittorrent.Misc.friendlyUnit(serverState.total_buffers_size, false));
936             $("WriteCacheOverload").set("html", serverState.write_cache_overload + "%");
937             $("ReadCacheOverload").set("html", serverState.read_cache_overload + "%");
938             $("QueuedIOJobs").set("html", serverState.queued_io_jobs);
939             $("AverageTimeInQueue").set("html", serverState.average_time_queue + " ms");
940             $("TotalQueuedSize").set("html", window.qBittorrent.Misc.friendlyUnit(serverState.total_queued_size, false));
941         }
943         switch (serverState.connection_status) {
944             case "connected":
945                 $("connectionStatus").src = "images/connected.svg";
946                 $("connectionStatus").alt = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
947                 $("connectionStatus").title = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
948                 break;
949             case "firewalled":
950                 $("connectionStatus").src = "images/firewalled.svg";
951                 $("connectionStatus").alt = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
952                 $("connectionStatus").title = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
953                 break;
954             default:
955                 $("connectionStatus").src = "images/disconnected.svg";
956                 $("connectionStatus").alt = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
957                 $("connectionStatus").title = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
958                 break;
959         }
961         if (queueing_enabled !== serverState.queueing) {
962             queueing_enabled = serverState.queueing;
963             torrentsTable.columns["priority"].force_hide = !queueing_enabled;
964             torrentsTable.updateColumn("priority");
965             if (queueing_enabled) {
966                 $("topQueuePosItem").removeClass("invisible");
967                 $("increaseQueuePosItem").removeClass("invisible");
968                 $("decreaseQueuePosItem").removeClass("invisible");
969                 $("bottomQueuePosItem").removeClass("invisible");
970                 $("queueingButtons").removeClass("invisible");
971                 $("queueingMenuItems").removeClass("invisible");
972             }
973             else {
974                 $("topQueuePosItem").addClass("invisible");
975                 $("increaseQueuePosItem").addClass("invisible");
976                 $("decreaseQueuePosItem").addClass("invisible");
977                 $("bottomQueuePosItem").addClass("invisible");
978                 $("queueingButtons").addClass("invisible");
979                 $("queueingMenuItems").addClass("invisible");
980             }
981         }
983         if (alternativeSpeedLimits !== serverState.use_alt_speed_limits) {
984             alternativeSpeedLimits = serverState.use_alt_speed_limits;
985             updateAltSpeedIcon(alternativeSpeedLimits);
986         }
988         if (useSubcategories !== serverState.use_subcategories) {
989             useSubcategories = serverState.use_subcategories;
990             updateCategoryList();
991         }
993         serverSyncMainDataInterval = Math.max(serverState.refresh_interval, 500);
994     };
996     const updateAltSpeedIcon = function(enabled) {
997         if (enabled) {
998             $("alternativeSpeedLimits").src = "images/slow.svg";
999             $("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
1000             $("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
1001         }
1002         else {
1003             $("alternativeSpeedLimits").src = "images/slow_off.svg";
1004             $("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
1005             $("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
1006         }
1007     };
1009     $("alternativeSpeedLimits").addEvent("click", () => {
1010         // Change icon immediately to give some feedback
1011         updateAltSpeedIcon(!alternativeSpeedLimits);
1013         new Request({
1014             url: "api/v2/transfer/toggleSpeedLimitsMode",
1015             method: "post",
1016             onComplete: function() {
1017                 alternativeSpeedLimits = !alternativeSpeedLimits;
1018                 updateMainData();
1019             },
1020             onFailure: function() {
1021                 // Restore icon in case of failure
1022                 updateAltSpeedIcon(alternativeSpeedLimits);
1023             }
1024         }).send();
1025     });
1027     $("DlInfos").addEvent("click", globalDownloadLimitFN);
1028     $("UpInfos").addEvent("click", globalUploadLimitFN);
1030     $("showTopToolbarLink").addEvent("click", (e) => {
1031         showTopToolbar = !showTopToolbar;
1032         LocalPreferences.set("show_top_toolbar", showTopToolbar.toString());
1033         if (showTopToolbar) {
1034             $("showTopToolbarLink").firstChild.style.opacity = "1";
1035             $("mochaToolbar").removeClass("invisible");
1036         }
1037         else {
1038             $("showTopToolbarLink").firstChild.style.opacity = "0";
1039             $("mochaToolbar").addClass("invisible");
1040         }
1041         MochaUI.Desktop.setDesktopSize();
1042     });
1044     $("showStatusBarLink").addEvent("click", (e) => {
1045         showStatusBar = !showStatusBar;
1046         LocalPreferences.set("show_status_bar", showStatusBar.toString());
1047         if (showStatusBar) {
1048             $("showStatusBarLink").firstChild.style.opacity = "1";
1049             $("desktopFooterWrapper").removeClass("invisible");
1050         }
1051         else {
1052             $("showStatusBarLink").firstChild.style.opacity = "0";
1053             $("desktopFooterWrapper").addClass("invisible");
1054         }
1055         MochaUI.Desktop.setDesktopSize();
1056     });
1058     const registerMagnetHandler = function() {
1059         if (typeof navigator.registerProtocolHandler !== "function") {
1060             if (window.location.protocol !== "https:")
1061                 alert("QBT_TR(To use this feature, the WebUI needs to be accessed over HTTPS)QBT_TR[CONTEXT=MainWindow]");
1062             else
1063                 alert("QBT_TR(Your browser does not support this feature)QBT_TR[CONTEXT=MainWindow]");
1064             return;
1065         }
1067         const hashString = location.hash ? location.hash.replace(/^#/, "") : "";
1068         const hashParams = new URLSearchParams(hashString);
1069         hashParams.set("download", "");
1071         const templateHashString = hashParams.toString().replace("download=", "download=%s");
1072         const templateUrl = location.origin + location.pathname
1073             + location.search + "#" + templateHashString;
1075         navigator.registerProtocolHandler("magnet", templateUrl,
1076             "qBittorrent WebUI magnet handler");
1077     };
1078     $("registerMagnetHandlerLink").addEvent("click", (e) => {
1079         registerMagnetHandler();
1080     });
1082     $("showFiltersSidebarLink").addEvent("click", (e) => {
1083         showFiltersSidebar = !showFiltersSidebar;
1084         LocalPreferences.set("show_filters_sidebar", showFiltersSidebar.toString());
1085         if (showFiltersSidebar) {
1086             $("showFiltersSidebarLink").firstChild.style.opacity = "1";
1087             $("filtersColumn").removeClass("invisible");
1088             $("filtersColumn_handle").removeClass("invisible");
1089         }
1090         else {
1091             $("showFiltersSidebarLink").firstChild.style.opacity = "0";
1092             $("filtersColumn").addClass("invisible");
1093             $("filtersColumn_handle").addClass("invisible");
1094         }
1095         MochaUI.Desktop.setDesktopSize();
1096     });
1098     $("speedInBrowserTitleBarLink").addEvent("click", (e) => {
1099         speedInTitle = !speedInTitle;
1100         LocalPreferences.set("speed_in_browser_title_bar", speedInTitle.toString());
1101         if (speedInTitle)
1102             $("speedInBrowserTitleBarLink").firstChild.style.opacity = "1";
1103         else
1104             $("speedInBrowserTitleBarLink").firstChild.style.opacity = "0";
1105         processServerState();
1106     });
1108     $("showSearchEngineLink").addEvent("click", (e) => {
1109         window.qBittorrent.Client.showSearchEngine(!window.qBittorrent.Client.isShowSearchEngine());
1110         LocalPreferences.set("show_search_engine", window.qBittorrent.Client.isShowSearchEngine().toString());
1111         updateTabDisplay();
1112     });
1114     $("showRssReaderLink").addEvent("click", (e) => {
1115         window.qBittorrent.Client.showRssReader(!window.qBittorrent.Client.isShowRssReader());
1116         LocalPreferences.set("show_rss_reader", window.qBittorrent.Client.isShowRssReader().toString());
1117         updateTabDisplay();
1118     });
1120     $("showLogViewerLink").addEvent("click", (e) => {
1121         window.qBittorrent.Client.showLogViewer(!window.qBittorrent.Client.isShowLogViewer());
1122         LocalPreferences.set("show_log_viewer", window.qBittorrent.Client.isShowLogViewer().toString());
1123         updateTabDisplay();
1124     });
1126     const updateTabDisplay = function() {
1127         if (window.qBittorrent.Client.isShowRssReader()) {
1128             $("showRssReaderLink").firstChild.style.opacity = "1";
1129             $("mainWindowTabs").removeClass("invisible");
1130             $("rssTabLink").removeClass("invisible");
1131             if (!MochaUI.Panels.instances.RssPanel)
1132                 addRssPanel();
1133         }
1134         else {
1135             $("showRssReaderLink").firstChild.style.opacity = "0";
1136             $("rssTabLink").addClass("invisible");
1137             if ($("rssTabLink").hasClass("selected"))
1138                 $("transfersTabLink").click();
1139         }
1141         if (window.qBittorrent.Client.isShowSearchEngine()) {
1142             $("showSearchEngineLink").firstChild.style.opacity = "1";
1143             $("mainWindowTabs").removeClass("invisible");
1144             $("searchTabLink").removeClass("invisible");
1145             if (!MochaUI.Panels.instances.SearchPanel)
1146                 addSearchPanel();
1147         }
1148         else {
1149             $("showSearchEngineLink").firstChild.style.opacity = "0";
1150             $("searchTabLink").addClass("invisible");
1151             if ($("searchTabLink").hasClass("selected"))
1152                 $("transfersTabLink").click();
1153         }
1155         if (window.qBittorrent.Client.isShowLogViewer()) {
1156             $("showLogViewerLink").firstChild.style.opacity = "1";
1157             $("mainWindowTabs").removeClass("invisible");
1158             $("logTabLink").removeClass("invisible");
1159             if (!MochaUI.Panels.instances.LogPanel)
1160                 addLogPanel();
1161         }
1162         else {
1163             $("showLogViewerLink").firstChild.style.opacity = "0";
1164             $("logTabLink").addClass("invisible");
1165             if ($("logTabLink").hasClass("selected"))
1166                 $("transfersTabLink").click();
1167         }
1169         // display no tabs
1170         if (!window.qBittorrent.Client.isShowRssReader() && !window.qBittorrent.Client.isShowSearchEngine() && !window.qBittorrent.Client.isShowLogViewer())
1171             $("mainWindowTabs").addClass("invisible");
1172     };
1174     $("StatisticsLink").addEvent("click", StatisticsLinkFN);
1176     // main window tabs
1178     const showTransfersTab = function() {
1179         const showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
1180         if (showFiltersSidebar) {
1181             $("filtersColumn").removeClass("invisible");
1182             $("filtersColumn_handle").removeClass("invisible");
1183         }
1184         $("mainColumn").removeClass("invisible");
1185         $("torrentsFilterToolbar").removeClass("invisible");
1187         customSyncMainDataInterval = null;
1188         syncData(100);
1190         hideSearchTab();
1191         hideRssTab();
1192         hideLogTab();
1194         LocalPreferences.set("selected_window_tab", "transfers");
1195     };
1197     const hideTransfersTab = function() {
1198         $("filtersColumn").addClass("invisible");
1199         $("filtersColumn_handle").addClass("invisible");
1200         $("mainColumn").addClass("invisible");
1201         $("torrentsFilterToolbar").addClass("invisible");
1202         MochaUI.Desktop.resizePanels();
1203     };
1205     const showSearchTab = (function() {
1206         let searchTabInitialized = false;
1208         return () => {
1209             // we must wait until the panel is fully loaded before proceeding.
1210             // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1211             // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1212             if (!isSearchPanelLoaded) {
1213                 setTimeout(() => {
1214                     showSearchTab();
1215                 }, 100);
1216                 return;
1217             }
1219             if (!searchTabInitialized) {
1220                 window.qBittorrent.Search.init();
1221                 searchTabInitialized = true;
1222             }
1224             $("searchTabColumn").removeClass("invisible");
1225             customSyncMainDataInterval = 30000;
1226             hideTransfersTab();
1227             hideRssTab();
1228             hideLogTab();
1230             LocalPreferences.set("selected_window_tab", "search");
1231         };
1232     })();
1234     const hideSearchTab = function() {
1235         $("searchTabColumn").addClass("invisible");
1236         MochaUI.Desktop.resizePanels();
1237     };
1239     const showRssTab = (function() {
1240         let rssTabInitialized = false;
1242         return () => {
1243             if (!rssTabInitialized) {
1244                 window.qBittorrent.Rss.init();
1245                 rssTabInitialized = true;
1246             }
1247             else {
1248                 window.qBittorrent.Rss.load();
1249             }
1251             $("rssTabColumn").removeClass("invisible");
1252             customSyncMainDataInterval = 30000;
1253             hideTransfersTab();
1254             hideSearchTab();
1255             hideLogTab();
1257             LocalPreferences.set("selected_window_tab", "rss");
1258         };
1259     })();
1261     const hideRssTab = function() {
1262         $("rssTabColumn").addClass("invisible");
1263         window.qBittorrent.Rss && window.qBittorrent.Rss.unload();
1264         MochaUI.Desktop.resizePanels();
1265     };
1267     const showLogTab = (function() {
1268         let logTabInitialized = false;
1270         return () => {
1271             // we must wait until the panel is fully loaded before proceeding.
1272             // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1273             // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1274             if (!isLogPanelLoaded) {
1275                 setTimeout(() => {
1276                     showLogTab();
1277                 }, 100);
1278                 return;
1279             }
1281             if (!logTabInitialized) {
1282                 window.qBittorrent.Log.init();
1283                 logTabInitialized = true;
1284             }
1285             else {
1286                 window.qBittorrent.Log.load();
1287             }
1289             $("logTabColumn").removeClass("invisible");
1290             customSyncMainDataInterval = 30000;
1291             hideTransfersTab();
1292             hideSearchTab();
1293             hideRssTab();
1295             LocalPreferences.set("selected_window_tab", "log");
1296         };
1297     })();
1299     const hideLogTab = function() {
1300         $("logTabColumn").addClass("invisible");
1301         MochaUI.Desktop.resizePanels();
1302         window.qBittorrent.Log && window.qBittorrent.Log.unload();
1303     };
1305     const addSearchPanel = function() {
1306         new MochaUI.Panel({
1307             id: "SearchPanel",
1308             title: "Search",
1309             header: false,
1310             padding: {
1311                 top: 0,
1312                 right: 0,
1313                 bottom: 0,
1314                 left: 0
1315             },
1316             loadMethod: "xhr",
1317             contentURL: "views/search.html",
1318             require: {
1319                 js: ["scripts/search.js"],
1320                 onload: () => {
1321                     isSearchPanelLoaded = true;
1322                 },
1323             },
1324             content: "",
1325             column: "searchTabColumn",
1326             height: null
1327         });
1328     };
1330     const addRssPanel = function() {
1331         new MochaUI.Panel({
1332             id: "RssPanel",
1333             title: "Rss",
1334             header: false,
1335             padding: {
1336                 top: 0,
1337                 right: 0,
1338                 bottom: 0,
1339                 left: 0
1340             },
1341             loadMethod: "xhr",
1342             contentURL: "views/rss.html",
1343             content: "",
1344             column: "rssTabColumn",
1345             height: null
1346         });
1347     };
1349     const addLogPanel = function() {
1350         new MochaUI.Panel({
1351             id: "LogPanel",
1352             title: "Log",
1353             header: true,
1354             padding: {
1355                 top: 0,
1356                 right: 0,
1357                 bottom: 0,
1358                 left: 0
1359             },
1360             loadMethod: "xhr",
1361             contentURL: "views/log.html",
1362             require: {
1363                 css: ["css/vanillaSelectBox.css"],
1364                 js: ["scripts/lib/vanillaSelectBox.js"],
1365                 onload: () => {
1366                     isLogPanelLoaded = true;
1367                 },
1368             },
1369             tabsURL: "views/logTabs.html",
1370             tabsOnload: function() {
1371                 MochaUI.initializeTabs("panelTabs");
1373                 $("logMessageLink").addEvent("click", (e) => {
1374                     window.qBittorrent.Log.setCurrentTab("main");
1375                 });
1377                 $("logPeerLink").addEvent("click", (e) => {
1378                     window.qBittorrent.Log.setCurrentTab("peer");
1379                 });
1380             },
1381             collapsible: false,
1382             content: "",
1383             column: "logTabColumn",
1384             height: null
1385         });
1386     };
1388     const handleDownloadParam = function() {
1389         // Extract torrent URL from download param in WebUI URL hash
1390         const downloadHash = "#download=";
1391         if (location.hash.indexOf(downloadHash) !== 0)
1392             return;
1394         const url = decodeURIComponent(location.hash.substring(downloadHash.length));
1395         // Remove the processed hash from the URL
1396         history.replaceState("", document.title, (location.pathname + location.search));
1397         showDownloadPage([url]);
1398     };
1400     new MochaUI.Panel({
1401         id: "transferList",
1402         title: "Panel",
1403         header: false,
1404         padding: {
1405             top: 0,
1406             right: 0,
1407             bottom: 0,
1408             left: 0
1409         },
1410         loadMethod: "xhr",
1411         contentURL: "views/transferlist.html",
1412         onContentLoaded: function() {
1413             handleDownloadParam();
1414             updateMainData();
1415         },
1416         column: "mainColumn",
1417         onResize: saveColumnSizes,
1418         height: null
1419     });
1420     let prop_h = LocalPreferences.get("properties_height_rel");
1421     if (prop_h !== null)
1422         prop_h = prop_h.toFloat() * Window.getSize().y;
1423     else
1424         prop_h = Window.getSize().y / 2.0;
1425     new MochaUI.Panel({
1426         id: "propertiesPanel",
1427         title: "Panel",
1428         header: true,
1429         padding: {
1430             top: 0,
1431             right: 0,
1432             bottom: 0,
1433             left: 0
1434         },
1435         contentURL: "views/properties.html",
1436         require: {
1437             css: ["css/Tabs.css", "css/dynamicTable.css"],
1438             js: ["scripts/prop-general.js", "scripts/prop-trackers.js", "scripts/prop-peers.js", "scripts/prop-webseeds.js", "scripts/prop-files.js"],
1439         },
1440         tabsURL: "views/propertiesToolbar.html",
1441         tabsOnload: function() {
1442             MochaUI.initializeTabs("propertiesTabs");
1444             updatePropertiesPanel = function() {
1445                 if (!$("prop_general").hasClass("invisible")) {
1446                     if (window.qBittorrent.PropGeneral !== undefined)
1447                         window.qBittorrent.PropGeneral.updateData();
1448                 }
1449                 else if (!$("prop_trackers").hasClass("invisible")) {
1450                     if (window.qBittorrent.PropTrackers !== undefined)
1451                         window.qBittorrent.PropTrackers.updateData();
1452                 }
1453                 else if (!$("prop_peers").hasClass("invisible")) {
1454                     if (window.qBittorrent.PropPeers !== undefined)
1455                         window.qBittorrent.PropPeers.updateData();
1456                 }
1457                 else if (!$("prop_webseeds").hasClass("invisible")) {
1458                     if (window.qBittorrent.PropWebseeds !== undefined)
1459                         window.qBittorrent.PropWebseeds.updateData();
1460                 }
1461                 else if (!$("prop_files").hasClass("invisible")) {
1462                     if (window.qBittorrent.PropFiles !== undefined)
1463                         window.qBittorrent.PropFiles.updateData();
1464                 }
1465             };
1467             $("PropGeneralLink").addEvent("click", function(e) {
1468                 $$(".propertiesTabContent").addClass("invisible");
1469                 $("prop_general").removeClass("invisible");
1470                 hideFilesFilter();
1471                 updatePropertiesPanel();
1472                 LocalPreferences.set("selected_tab", this.id);
1473             });
1475             $("PropTrackersLink").addEvent("click", function(e) {
1476                 $$(".propertiesTabContent").addClass("invisible");
1477                 $("prop_trackers").removeClass("invisible");
1478                 hideFilesFilter();
1479                 updatePropertiesPanel();
1480                 LocalPreferences.set("selected_tab", this.id);
1481             });
1483             $("PropPeersLink").addEvent("click", function(e) {
1484                 $$(".propertiesTabContent").addClass("invisible");
1485                 $("prop_peers").removeClass("invisible");
1486                 hideFilesFilter();
1487                 updatePropertiesPanel();
1488                 LocalPreferences.set("selected_tab", this.id);
1489             });
1491             $("PropWebSeedsLink").addEvent("click", function(e) {
1492                 $$(".propertiesTabContent").addClass("invisible");
1493                 $("prop_webseeds").removeClass("invisible");
1494                 hideFilesFilter();
1495                 updatePropertiesPanel();
1496                 LocalPreferences.set("selected_tab", this.id);
1497             });
1499             $("PropFilesLink").addEvent("click", function(e) {
1500                 $$(".propertiesTabContent").addClass("invisible");
1501                 $("prop_files").removeClass("invisible");
1502                 showFilesFilter();
1503                 updatePropertiesPanel();
1504                 LocalPreferences.set("selected_tab", this.id);
1505             });
1507             $("propertiesPanel_collapseToggle").addEvent("click", (e) => {
1508                 updatePropertiesPanel();
1509             });
1510         },
1511         column: "mainColumn",
1512         height: prop_h
1513     });
1515     const showFilesFilter = function() {
1516         $("torrentFilesFilterToolbar").removeClass("invisible");
1517     };
1519     const hideFilesFilter = function() {
1520         $("torrentFilesFilterToolbar").addClass("invisible");
1521     };
1523     // listen for changes to torrentsFilterInput
1524     let torrentsFilterInputTimer = -1;
1525     $("torrentsFilterInput").addEvent("input", () => {
1526         clearTimeout(torrentsFilterInputTimer);
1527         torrentsFilterInputTimer = setTimeout(() => {
1528             torrentsFilterInputTimer = -1;
1529             torrentsTable.updateTable();
1530         }, window.qBittorrent.Misc.FILTER_INPUT_DELAY);
1531     });
1532     $("torrentsFilterRegexBox").addEvent("change", () => {
1533         torrentsTable.updateTable();
1534     });
1536     $("transfersTabLink").addEvent("click", showTransfersTab);
1537     $("searchTabLink").addEvent("click", showSearchTab);
1538     $("rssTabLink").addEvent("click", showRssTab);
1539     $("logTabLink").addEvent("click", showLogTab);
1540     updateTabDisplay();
1542     const registerDragAndDrop = () => {
1543         $("desktop").addEventListener("dragover", (ev) => {
1544             if (ev.preventDefault)
1545                 ev.preventDefault();
1546         });
1548         $("desktop").addEventListener("dragenter", (ev) => {
1549             if (ev.preventDefault)
1550                 ev.preventDefault();
1551         });
1553         $("desktop").addEventListener("drop", (ev) => {
1554             if (ev.preventDefault)
1555                 ev.preventDefault();
1557             const droppedFiles = ev.dataTransfer.files;
1559             if (droppedFiles.length > 0) {
1560                 // dropped files or folders
1562                 // can't handle folder due to cannot put the filelist (from dropped folder)
1563                 // to <input> `files` field
1564                 for (const item of ev.dataTransfer.items) {
1565                     if (item.webkitGetAsEntry().isDirectory)
1566                         return;
1567                 }
1569                 const id = "uploadPage";
1570                 new MochaUI.Window({
1571                     id: id,
1572                     title: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
1573                     loadMethod: "iframe",
1574                     contentURL: new URI("upload.html").toString(),
1575                     addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
1576                     scrollbars: true,
1577                     maximizable: false,
1578                     paddingVertical: 0,
1579                     paddingHorizontal: 0,
1580                     width: loadWindowWidth(id, 500),
1581                     height: loadWindowHeight(id, 460),
1582                     onResize: () => {
1583                         saveWindowSize(id);
1584                     },
1585                     onContentLoaded: () => {
1586                         const fileInput = $(`${id}_iframe`).contentDocument.getElementById("fileselect");
1587                         fileInput.files = droppedFiles;
1588                     }
1589                 });
1590             }
1592             const droppedText = ev.dataTransfer.getData("text");
1593             if (droppedText.length > 0) {
1594                 // dropped text
1596                 const urls = droppedText.split("\n")
1597                     .map((str) => str.trim())
1598                     .filter((str) => {
1599                         const lowercaseStr = str.toLowerCase();
1600                         return lowercaseStr.startsWith("http:")
1601                             || lowercaseStr.startsWith("https:")
1602                             || lowercaseStr.startsWith("magnet:")
1603                             || ((str.length === 40) && !(/[^0-9A-F]/i.test(str))) // v1 hex-encoded SHA-1 info-hash
1604                             || ((str.length === 32) && !(/[^2-7A-Z]/i.test(str))); // v1 Base32 encoded SHA-1 info-hash
1605                     });
1607                 if (urls.length <= 0)
1608                     return;
1610                 const id = "downloadPage";
1611                 const contentURI = new URI("download.html").setData("urls", urls.map(encodeURIComponent).join("|"));
1612                 new MochaUI.Window({
1613                     id: id,
1614                     title: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
1615                     loadMethod: "iframe",
1616                     contentURL: contentURI.toString(),
1617                     addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
1618                     scrollbars: true,
1619                     maximizable: false,
1620                     closable: true,
1621                     paddingVertical: 0,
1622                     paddingHorizontal: 0,
1623                     width: loadWindowWidth(id, 500),
1624                     height: loadWindowHeight(id, 600),
1625                     onResize: () => {
1626                         saveWindowSize(id);
1627                     }
1628                 });
1629             }
1630         });
1631     };
1632     registerDragAndDrop();
1634     new Keyboard({
1635         defaultEventType: "keydown",
1636         events: {
1637             "ctrl+a": function(event) {
1638                 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1639                     return;
1640                 if (event.target.isContentEditable)
1641                     return;
1642                 torrentsTable.selectAll();
1643                 event.preventDefault();
1644             },
1645             "delete": function(event) {
1646                 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1647                     return;
1648                 if (event.target.isContentEditable)
1649                     return;
1650                 deleteFN();
1651                 event.preventDefault();
1652             },
1653             "shift+delete": (event) => {
1654                 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1655                     return;
1656                 if (event.target.isContentEditable)
1657                     return;
1658                 deleteFN(true);
1659                 event.preventDefault();
1660             }
1661         }
1662     }).activate();
1665 window.addEventListener("load", () => {
1666     // fetch various data and store it in memory
1667     window.qBittorrent.Cache.buildInfo.init();
1668     window.qBittorrent.Cache.preferences.init();
1669     window.qBittorrent.Cache.qbtVersion.init();
1671     // switch to previously used tab
1672     const previouslyUsedTab = LocalPreferences.get("selected_window_tab", "transfers");
1673     switch (previouslyUsedTab) {
1674         case "search":
1675             if (window.qBittorrent.Client.isShowSearchEngine())
1676                 $("searchTabLink").click();
1677             break;
1678         case "rss":
1679             if (window.qBittorrent.Client.isShowRssReader())
1680                 $("rssTabLink").click();
1681             break;
1682         case "log":
1683             if (window.qBittorrent.Client.isShowLogViewer())
1684                 $("logTabLink").click();
1685             break;
1686         case "transfers":
1687             $("transfersTabLink").click();
1688             break;
1689         default:
1690             console.error(`Unexpected 'selected_window_tab' value: ${previouslyUsedTab}`);
1691             $("transfersTabLink").click();
1692             break;
1693     };