WebUI: Add missing icons
[qBittorrent.git] / src / webui / www / private / scripts / client.js
blobdfdad4cb6f333edd087ee5a4411b516b6e416a08
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 window.qBittorrent ??= {};
29 window.qBittorrent.Client ??= (() => {
30     const exports = () => {
31         return {
32             closeWindows: closeWindows,
33             genHash: genHash,
34             getSyncMainDataInterval: getSyncMainDataInterval,
35             isStopped: isStopped,
36             stop: stop,
37             mainTitle: mainTitle,
38             showSearchEngine: showSearchEngine,
39             showRssReader: showRssReader,
40             showLogViewer: showLogViewer,
41             isShowSearchEngine: isShowSearchEngine,
42             isShowRssReader: isShowRssReader,
43             isShowLogViewer: isShowLogViewer
44         };
45     };
47     const closeWindows = function() {
48         MochaUI.closeAll();
49     };
51     const genHash = function(string) {
52         // origins:
53         // https://stackoverflow.com/a/8831937
54         // https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0
55         let hash = 0;
56         for (let i = 0; i < string.length; ++i)
57             hash = ((Math.imul(hash, 31) + string.charCodeAt(i)) | 0);
58         return hash;
59     };
61     const getSyncMainDataInterval = function() {
62         return customSyncMainDataInterval ? customSyncMainDataInterval : serverSyncMainDataInterval;
63     };
65     let stopped = false;
66     const isStopped = () => {
67         return stopped;
68     };
70     const stop = () => {
71         stopped = true;
72     };
74     const mainTitle = () => {
75         const emDash = "\u2014";
76         const qbtVersion = window.qBittorrent.Cache.qbtVersion.get();
77         const suffix = window.qBittorrent.Cache.preferences.get()["app_instance_name"] || "";
78         const title = `qBittorrent ${qbtVersion} QBT_TR(WebUI)QBT_TR[CONTEXT=OptionsDialog]`
79             + ((suffix.length > 0) ? ` ${emDash} ${suffix}` : "");
80         return title;
81     };
83     let showingSearchEngine = false;
84     let showingRssReader = false;
85     let showingLogViewer = false;
87     const showSearchEngine = function(bool) {
88         showingSearchEngine = bool;
89     };
90     const showRssReader = function(bool) {
91         showingRssReader = bool;
92     };
93     const showLogViewer = function(bool) {
94         showingLogViewer = bool;
95     };
96     const isShowSearchEngine = function() {
97         return showingSearchEngine;
98     };
99     const isShowRssReader = function() {
100         return showingRssReader;
101     };
102     const isShowLogViewer = function() {
103         return showingLogViewer;
104     };
106     return exports();
107 })();
108 Object.freeze(window.qBittorrent.Client);
110 // TODO: move global functions/variables into some namespace/scope
112 this.torrentsTable = new window.qBittorrent.DynamicTable.TorrentsTable();
114 let updatePropertiesPanel = function() {};
116 this.updateMainData = function() {};
117 let alternativeSpeedLimits = false;
118 let queueing_enabled = true;
119 let serverSyncMainDataInterval = 1500;
120 let customSyncMainDataInterval = null;
121 let useSubcategories = true;
122 const useAutoHideZeroStatusFilters = LocalPreferences.get("hide_zero_status_filters", "false") === "true";
124 /* Categories filter */
125 const CATEGORIES_ALL = 1;
126 const CATEGORIES_UNCATEGORIZED = 2;
128 const category_list = new Map();
130 let selected_category = Number(LocalPreferences.get("selected_category", CATEGORIES_ALL));
131 let setCategoryFilter = function() {};
133 /* Tags filter */
134 const TAGS_ALL = 1;
135 const TAGS_UNTAGGED = 2;
137 const tagList = new Map();
139 let selectedTag = Number(LocalPreferences.get("selected_tag", TAGS_ALL));
140 let setTagFilter = function() {};
142 /* Trackers filter */
143 const TRACKERS_ALL = 1;
144 const TRACKERS_TRACKERLESS = 2;
146 /** @type Map<number, {host: string, trackerTorrentMap: Map<string, string[]>}> **/
147 const trackerList = new Map();
149 let selectedTracker = LocalPreferences.get("selected_tracker", TRACKERS_ALL);
150 let setTrackerFilter = function() {};
152 /* All filters */
153 let selected_filter = LocalPreferences.get("selected_filter", "all");
154 let setFilter = function() {};
155 let toggleFilterDisplay = function() {};
157 window.addEventListener("DOMContentLoaded", () => {
158     let isSearchPanelLoaded = false;
159     let isLogPanelLoaded = false;
161     const saveColumnSizes = function() {
162         const filters_width = $("Filters").getSize().x;
163         LocalPreferences.set("filters_width", filters_width);
164         const properties_height_rel = $("propertiesPanel").getSize().y / Window.getSize().y;
165         LocalPreferences.set("properties_height_rel", properties_height_rel);
166     };
168     window.addEventListener("resize", () => {
169         // only save sizes if the columns are visible
170         if (!$("mainColumn").hasClass("invisible"))
171             saveColumnSizes.delay(200); // Resizing might takes some time.
172     });
174     /* MochaUI.Desktop = new MochaUI.Desktop();
175     MochaUI.Desktop.desktop.style.background = "#fff";
176     MochaUI.Desktop.desktop.style.visibility = "visible"; */
177     MochaUI.Desktop.initialize();
179     const buildTransfersTab = function() {
180         const filt_w = Number(LocalPreferences.get("filters_width", 120));
181         new MochaUI.Column({
182             id: "filtersColumn",
183             placement: "left",
184             onResize: saveColumnSizes,
185             width: filt_w,
186             resizeLimit: [1, 300]
187         });
188         new MochaUI.Column({
189             id: "mainColumn",
190             placement: "main"
191         });
192     };
194     const buildSearchTab = function() {
195         new MochaUI.Column({
196             id: "searchTabColumn",
197             placement: "main",
198             width: null
199         });
201         // start off hidden
202         $("searchTabColumn").addClass("invisible");
203     };
205     const buildRssTab = function() {
206         new MochaUI.Column({
207             id: "rssTabColumn",
208             placement: "main",
209             width: null
210         });
212         // start off hidden
213         $("rssTabColumn").addClass("invisible");
214     };
216     const buildLogTab = function() {
217         new MochaUI.Column({
218             id: "logTabColumn",
219             placement: "main",
220             width: null
221         });
223         // start off hidden
224         $("logTabColumn").addClass("invisible");
225     };
227     buildTransfersTab();
228     buildSearchTab();
229     buildRssTab();
230     buildLogTab();
231     MochaUI.initializeTabs("mainWindowTabsList");
233     setCategoryFilter = function(hash) {
234         selected_category = hash;
235         LocalPreferences.set("selected_category", selected_category);
236         highlightSelectedCategory();
237         if (typeof torrentsTable.tableBody !== "undefined")
238             updateMainData();
239     };
241     setTagFilter = function(hash) {
242         selectedTag = hash;
243         LocalPreferences.set("selected_tag", selectedTag);
244         highlightSelectedTag();
245         if (torrentsTable.tableBody !== undefined)
246             updateMainData();
247     };
249     setTrackerFilter = function(hash) {
250         selectedTracker = hash.toString();
251         LocalPreferences.set("selected_tracker", selectedTracker);
252         highlightSelectedTracker();
253         if (torrentsTable.tableBody !== undefined)
254             updateMainData();
255     };
257     setFilter = function(f) {
258         // Visually Select the right filter
259         $("all_filter").removeClass("selectedFilter");
260         $("downloading_filter").removeClass("selectedFilter");
261         $("seeding_filter").removeClass("selectedFilter");
262         $("completed_filter").removeClass("selectedFilter");
263         $("stopped_filter").removeClass("selectedFilter");
264         $("running_filter").removeClass("selectedFilter");
265         $("active_filter").removeClass("selectedFilter");
266         $("inactive_filter").removeClass("selectedFilter");
267         $("stalled_filter").removeClass("selectedFilter");
268         $("stalled_uploading_filter").removeClass("selectedFilter");
269         $("stalled_downloading_filter").removeClass("selectedFilter");
270         $("checking_filter").removeClass("selectedFilter");
271         $("moving_filter").removeClass("selectedFilter");
272         $("errored_filter").removeClass("selectedFilter");
273         $(f + "_filter").addClass("selectedFilter");
274         selected_filter = f;
275         LocalPreferences.set("selected_filter", f);
276         // Reload torrents
277         if (typeof torrentsTable.tableBody !== "undefined")
278             updateMainData();
279     };
281     toggleFilterDisplay = function(filter) {
282         const element = filter + "FilterList";
283         LocalPreferences.set("filter_" + filter + "_collapsed", !$(element).hasClass("invisible"));
284         $(element).toggleClass("invisible");
285         const parent = $(element).getParent(".filterWrapper");
286         const toggleIcon = $(parent).getChildren(".filterTitle img");
287         if (toggleIcon)
288             toggleIcon[0].toggleClass("rotate");
289     };
291     new MochaUI.Panel({
292         id: "Filters",
293         title: "Panel",
294         header: false,
295         padding: {
296             top: 0,
297             right: 0,
298             bottom: 0,
299             left: 0
300         },
301         loadMethod: "xhr",
302         contentURL: "views/filters.html",
303         onContentLoaded: function() {
304             setFilter(selected_filter);
305         },
306         column: "filtersColumn",
307         height: 300
308     });
309     initializeWindows();
311     // Show Top Toolbar is enabled by default
312     let showTopToolbar = LocalPreferences.get("show_top_toolbar", "true") === "true";
313     if (!showTopToolbar) {
314         $("showTopToolbarLink").firstChild.style.opacity = "0";
315         $("mochaToolbar").addClass("invisible");
316     }
318     // Show Status Bar is enabled by default
319     let showStatusBar = LocalPreferences.get("show_status_bar", "true") === "true";
320     if (!showStatusBar) {
321         $("showStatusBarLink").firstChild.style.opacity = "0";
322         $("desktopFooterWrapper").addClass("invisible");
323     }
325     // Show Filters Sidebar is enabled by default
326     let showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
327     if (!showFiltersSidebar) {
328         $("showFiltersSidebarLink").firstChild.style.opacity = "0";
329         $("filtersColumn").addClass("invisible");
330         $("filtersColumn_handle").addClass("invisible");
331     }
333     let speedInTitle = LocalPreferences.get("speed_in_browser_title_bar") === "true";
334     if (!speedInTitle)
335         $("speedInBrowserTitleBarLink").firstChild.style.opacity = "0";
337     // After showing/hiding the toolbar + status bar
338     window.qBittorrent.Client.showSearchEngine(LocalPreferences.get("show_search_engine") !== "false");
339     window.qBittorrent.Client.showRssReader(LocalPreferences.get("show_rss_reader") !== "false");
340     window.qBittorrent.Client.showLogViewer(LocalPreferences.get("show_log_viewer") === "true");
342     // After Show Top Toolbar
343     MochaUI.Desktop.setDesktopSize();
345     let syncMainDataLastResponseId = 0;
346     const serverState = {};
348     const removeTorrentFromCategoryList = function(hash) {
349         if (!hash)
350             return false;
352         let removed = false;
353         category_list.forEach((category) => {
354             const deleteResult = category.torrents.delete(hash);
355             removed ||= deleteResult;
356         });
358         return removed;
359     };
361     const addTorrentToCategoryList = function(torrent) {
362         const category = torrent["category"];
363         if (typeof category === "undefined")
364             return false;
366         const hash = torrent["hash"];
367         if (category.length === 0) { // Empty category
368             removeTorrentFromCategoryList(hash);
369             return true;
370         }
372         const categoryHash = window.qBittorrent.Client.genHash(category);
373         if (!category_list.has(categoryHash)) { // This should not happen
374             category_list.set(categoryHash, {
375                 name: category,
376                 torrents: new Set()
377             });
378         }
380         const torrents = category_list.get(categoryHash).torrents;
381         if (!torrents.has(hash)) {
382             removeTorrentFromCategoryList(hash);
383             torrents.add(hash);
384             return true;
385         }
386         return false;
387     };
389     const removeTorrentFromTagList = function(hash) {
390         if (!hash)
391             return false;
393         let removed = false;
394         tagList.forEach((tag) => {
395             const deleteResult = tag.torrents.delete(hash);
396             removed ||= deleteResult;
397         });
399         return removed;
400     };
402     const addTorrentToTagList = function(torrent) {
403         if (torrent["tags"] === undefined) // Tags haven't changed
404             return false;
406         const hash = torrent["hash"];
407         removeTorrentFromTagList(hash);
409         if (torrent["tags"].length === 0) // No tags
410             return true;
412         const tags = torrent["tags"].split(",");
413         let added = false;
414         for (let i = 0; i < tags.length; ++i) {
415             const tagHash = window.qBittorrent.Client.genHash(tags[i].trim());
416             if (!tagList.has(tagHash)) { // This should not happen
417                 tagList.set(tagHash, {
418                     name: tags,
419                     torrents: new Set()
420                 });
421             }
423             const torrents = tagList.get(tagHash).torrents;
424             if (!torrents.has(hash)) {
425                 torrents.add(hash);
426                 added = true;
427             }
428         }
429         return added;
430     };
432     const updateFilter = function(filter, filterTitle) {
433         const filterEl = document.getElementById(`${filter}_filter`);
434         const filterTorrentCount = torrentsTable.getFilteredTorrentsNumber(filter, CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
435         if (useAutoHideZeroStatusFilters) {
436             const hideFilter = (filterTorrentCount === 0) && (filter !== "all");
437             if (filterEl.classList.toggle("invisible", hideFilter))
438                 return;
439         }
440         filterEl.firstElementChild.lastChild.nodeValue = filterTitle.replace("%1", filterTorrentCount);
441     };
443     const updateFiltersList = function() {
444         updateFilter("all", "QBT_TR(All (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
445         updateFilter("downloading", "QBT_TR(Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
446         updateFilter("seeding", "QBT_TR(Seeding (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
447         updateFilter("completed", "QBT_TR(Completed (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
448         updateFilter("running", "QBT_TR(Running (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
449         updateFilter("stopped", "QBT_TR(Stopped (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
450         updateFilter("active", "QBT_TR(Active (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
451         updateFilter("inactive", "QBT_TR(Inactive (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
452         updateFilter("stalled", "QBT_TR(Stalled (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
453         updateFilter("stalled_uploading", "QBT_TR(Stalled Uploading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
454         updateFilter("stalled_downloading", "QBT_TR(Stalled Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
455         updateFilter("checking", "QBT_TR(Checking (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
456         updateFilter("moving", "QBT_TR(Moving (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
457         updateFilter("errored", "QBT_TR(Errored (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
458     };
460     const updateCategoryList = function() {
461         const categoryList = $("categoryFilterList");
462         if (!categoryList)
463             return;
464         categoryList.getChildren().each(c => c.destroy());
466         const create_link = function(hash, text, count) {
467             let display_name = text;
468             let margin_left = 0;
469             if (useSubcategories) {
470                 const category_path = text.split("/");
471                 display_name = category_path[category_path.length - 1];
472                 margin_left = (category_path.length - 1) * 20;
473             }
475             const span = document.createElement("span");
476             span.classList.add("link");
477             span.href = "#";
478             span.style.marginLeft = `${margin_left}px`;
479             span.textContent = `${display_name} (${count})`;
480             span.addEventListener("click", (event) => {
481                 event.preventDefault();
482                 setCategoryFilter(hash);
483             });
485             const img = document.createElement("img");
486             img.src = "images/view-categories.svg";
487             span.prepend(img);
489             const listItem = document.createElement("li");
490             listItem.id = hash;
491             listItem.appendChild(span);
493             window.qBittorrent.Filters.categoriesFilterContextMenu.addTarget(listItem);
494             return listItem;
495         };
497         const all = torrentsTable.getRowIds().length;
498         let uncategorized = 0;
499         for (const key in torrentsTable.rows) {
500             if (!Object.hasOwn(torrentsTable.rows, key))
501                 continue;
503             const row = torrentsTable.rows[key];
504             if (row["full_data"].category.length === 0)
505                 uncategorized += 1;
506         }
507         categoryList.appendChild(create_link(CATEGORIES_ALL, "QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]", all));
508         categoryList.appendChild(create_link(CATEGORIES_UNCATEGORIZED, "QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]", uncategorized));
510         const sortedCategories = [];
511         category_list.forEach((category, hash) => sortedCategories.push({
512             categoryName: category.name,
513             categoryHash: hash,
514             categoryCount: category.torrents.size
515         }));
516         sortedCategories.sort((left, right) => {
517             const leftSegments = left.categoryName.split("/");
518             const rightSegments = right.categoryName.split("/");
520             for (let i = 0, iMax = Math.min(leftSegments.length, rightSegments.length); i < iMax; ++i) {
521                 const compareResult = window.qBittorrent.Misc.naturalSortCollator.compare(
522                     leftSegments[i], rightSegments[i]);
523                 if (compareResult !== 0)
524                     return compareResult;
525             }
527             return leftSegments.length - rightSegments.length;
528         });
530         for (let i = 0; i < sortedCategories.length; ++i) {
531             const { categoryName, categoryHash } = sortedCategories[i];
532             let { categoryCount } = sortedCategories[i];
534             if (useSubcategories) {
535                 for (let j = (i + 1);
536                     ((j < sortedCategories.length) && sortedCategories[j].categoryName.startsWith(categoryName + "/")); ++j)
537                     categoryCount += sortedCategories[j].categoryCount;
538             }
540             categoryList.appendChild(create_link(categoryHash, categoryName, categoryCount));
541         }
543         highlightSelectedCategory();
544     };
546     const highlightSelectedCategory = function() {
547         const categoryList = $("categoryFilterList");
548         if (!categoryList)
549             return;
550         const children = categoryList.childNodes;
551         for (let i = 0; i < children.length; ++i) {
552             if (Number(children[i].id) === selected_category)
553                 children[i].className = "selectedFilter";
554             else
555                 children[i].className = "";
556         }
557     };
559     const updateTagList = function() {
560         const tagFilterList = $("tagFilterList");
561         if (tagFilterList === null)
562             return;
564         tagFilterList.getChildren().each(c => c.destroy());
566         const createLink = function(hash, text, count) {
567             const span = document.createElement("span");
568             span.classList.add("link");
569             span.href = "#";
570             span.textContent = `${text} (${count})`;
571             span.addEventListener("click", (event) => {
572                 event.preventDefault();
573                 setTagFilter(hash);
574             });
576             const img = document.createElement("img");
577             img.src = "images/tags.svg";
578             span.prepend(img);
580             const listItem = document.createElement("li");
581             listItem.id = hash;
582             listItem.appendChild(span);
584             window.qBittorrent.Filters.tagsFilterContextMenu.addTarget(listItem);
585             return listItem;
586         };
588         const torrentsCount = torrentsTable.getRowIds().length;
589         let untagged = 0;
590         for (const key in torrentsTable.rows) {
591             if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]["full_data"].tags.length === 0))
592                 untagged += 1;
593         }
594         tagFilterList.appendChild(createLink(TAGS_ALL, "QBT_TR(All)QBT_TR[CONTEXT=TagFilterModel]", torrentsCount));
595         tagFilterList.appendChild(createLink(TAGS_UNTAGGED, "QBT_TR(Untagged)QBT_TR[CONTEXT=TagFilterModel]", untagged));
597         const sortedTags = [];
598         tagList.forEach((tag, hash) => sortedTags.push({
599             tagName: tag.name,
600             tagHash: hash,
601             tagSize: tag.torrents.size
602         }));
603         sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName));
605         for (const { tagName, tagHash, tagSize } of sortedTags)
606             tagFilterList.appendChild(createLink(tagHash, tagName, tagSize));
608         highlightSelectedTag();
609     };
611     const highlightSelectedTag = function() {
612         const tagFilterList = $("tagFilterList");
613         if (!tagFilterList)
614             return;
616         const children = tagFilterList.childNodes;
617         for (let i = 0; i < children.length; ++i)
618             children[i].className = (Number(children[i].id) === selectedTag) ? "selectedFilter" : "";
619     };
621     // getHost emulate the GUI version `QString getHost(const QString &url)`
622     const getHost = function(url) {
623         // We want the hostname.
624         // If failed to parse the domain, original input should be returned
626         if (!/^(?:https?|udp):/i.test(url))
627             return url;
629         try {
630             // hack: URL can not get hostname from udp protocol
631             const parsedUrl = new URL(url.replace(/^udp:/i, "https:"));
632             // host: "example.com:8443"
633             // hostname: "example.com"
634             const host = parsedUrl.hostname;
635             if (!host)
636                 return url;
638             return host;
639         }
640         catch (error) {
641             return url;
642         }
643     };
645     const updateTrackerList = function() {
646         const trackerFilterList = $("trackerFilterList");
647         if (trackerFilterList === null)
648             return;
650         trackerFilterList.getChildren().each(c => c.destroy());
652         const createLink = function(hash, text, count) {
653             const span = document.createElement("span");
654             span.classList.add("link");
655             span.href = "#";
656             span.textContent = text.replace("%1", count);
657             span.addEventListener("click", (event) => {
658                 event.preventDefault();
659                 setTrackerFilter(hash);
660             });
662             const img = document.createElement("img");
663             img.src = "images/trackers.svg";
664             span.prepend(img);
666             const listItem = document.createElement("li");
667             listItem.id = hash;
668             listItem.appendChild(span);
670             window.qBittorrent.Filters.trackersFilterContextMenu.addTarget(listItem);
671             return listItem;
672         };
674         const torrentsCount = torrentsTable.getRowIds().length;
675         trackerFilterList.appendChild(createLink(TRACKERS_ALL, "QBT_TR(All (%1))QBT_TR[CONTEXT=TrackerFiltersList]", torrentsCount));
676         let trackerlessTorrentsCount = 0;
677         for (const key in torrentsTable.rows) {
678             if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]["full_data"].trackers_count === 0))
679                 trackerlessTorrentsCount += 1;
680         }
681         trackerFilterList.appendChild(createLink(TRACKERS_TRACKERLESS, "QBT_TR(Trackerless (%1))QBT_TR[CONTEXT=TrackerFiltersList]", trackerlessTorrentsCount));
683         // Sort trackers by hostname
684         const sortedList = [];
685         trackerList.forEach(({ host, trackerTorrentMap }, hash) => {
686             const uniqueTorrents = new Set();
687             for (const torrents of trackerTorrentMap.values()) {
688                 for (const torrent of torrents)
689                     uniqueTorrents.add(torrent);
690             }
692             sortedList.push({
693                 trackerHost: host,
694                 trackerHash: hash,
695                 trackerCount: uniqueTorrents.size,
696             });
697         });
698         sortedList.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.trackerHost, right.trackerHost));
699         for (const { trackerHost, trackerHash, trackerCount } of sortedList)
700             trackerFilterList.appendChild(createLink(trackerHash, (trackerHost + " (%1)"), trackerCount));
702         highlightSelectedTracker();
703     };
705     const highlightSelectedTracker = function() {
706         const trackerFilterList = $("trackerFilterList");
707         if (!trackerFilterList)
708             return;
710         const children = trackerFilterList.childNodes;
711         for (const child of children)
712             child.className = (child.id === selectedTracker) ? "selectedFilter" : "";
713     };
715     const setupCopyEventHandler = (function() {
716         let clipboardEvent;
718         return () => {
719             if (clipboardEvent)
720                 clipboardEvent.destroy();
722             clipboardEvent = new ClipboardJS(".copyToClipboard", {
723                 text: function(trigger) {
724                     switch (trigger.id) {
725                         case "copyName":
726                             return copyNameFN();
727                         case "copyInfohash1":
728                             return copyInfohashFN(1);
729                         case "copyInfohash2":
730                             return copyInfohashFN(2);
731                         case "copyMagnetLink":
732                             return copyMagnetLinkFN();
733                         case "copyID":
734                             return copyIdFN();
735                         case "copyComment":
736                             return copyCommentFN();
737                         default:
738                             return "";
739                     }
740                 }
741             });
742         };
743     })();
745     let syncMainDataTimeoutID = -1;
746     let syncRequestInProgress = false;
747     const syncMainData = function() {
748         const url = new URI("api/v2/sync/maindata");
749         url.setData("rid", syncMainDataLastResponseId);
750         const request = new Request.JSON({
751             url: url,
752             noCache: true,
753             method: "get",
754             onFailure: function() {
755                 const errorDiv = $("error_div");
756                 if (errorDiv)
757                     errorDiv.textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]";
758                 syncRequestInProgress = false;
759                 syncData(2000);
760             },
761             onSuccess: function(response) {
762                 $("error_div").textContent = "";
763                 if (response) {
764                     clearTimeout(torrentsFilterInputTimer);
765                     torrentsFilterInputTimer = -1;
767                     let torrentsTableSelectedRows;
768                     let update_categories = false;
769                     let updateTags = false;
770                     let updateTrackers = false;
771                     const full_update = (response["full_update"] === true);
772                     if (full_update) {
773                         torrentsTableSelectedRows = torrentsTable.selectedRowsIds();
774                         update_categories = true;
775                         updateTags = true;
776                         updateTrackers = true;
777                         torrentsTable.clear();
778                         category_list.clear();
779                         tagList.clear();
780                         trackerList.clear();
781                     }
782                     if (response["rid"])
783                         syncMainDataLastResponseId = response["rid"];
784                     if (response["categories"]) {
785                         for (const key in response["categories"]) {
786                             if (!Object.hasOwn(response["categories"], key))
787                                 continue;
789                             const responseCategory = response["categories"][key];
790                             const categoryHash = window.qBittorrent.Client.genHash(key);
791                             const category = category_list.get(categoryHash);
792                             if (category !== undefined) {
793                                 // only the save path can change for existing categories
794                                 category.savePath = responseCategory.savePath;
795                             }
796                             else {
797                                 category_list.set(categoryHash, {
798                                     name: responseCategory.name,
799                                     savePath: responseCategory.savePath,
800                                     torrents: new Set()
801                                 });
802                             }
803                         }
804                         update_categories = true;
805                     }
806                     if (response["categories_removed"]) {
807                         response["categories_removed"].each((category) => {
808                             const categoryHash = window.qBittorrent.Client.genHash(category);
809                             category_list.delete(categoryHash);
810                         });
811                         update_categories = true;
812                     }
813                     if (response["tags"]) {
814                         for (const tag of response["tags"]) {
815                             const tagHash = window.qBittorrent.Client.genHash(tag);
816                             if (!tagList.has(tagHash)) {
817                                 tagList.set(tagHash, {
818                                     name: tag,
819                                     torrents: new Set()
820                                 });
821                             }
822                         }
823                         updateTags = true;
824                     }
825                     if (response["tags_removed"]) {
826                         for (let i = 0; i < response["tags_removed"].length; ++i) {
827                             const tagHash = window.qBittorrent.Client.genHash(response["tags_removed"][i]);
828                             tagList.delete(tagHash);
829                         }
830                         updateTags = true;
831                     }
832                     if (response["trackers"]) {
833                         for (const [tracker, torrents] of Object.entries(response["trackers"])) {
834                             const host = getHost(tracker);
835                             const hash = window.qBittorrent.Client.genHash(host);
837                             let trackerListItem = trackerList.get(hash);
838                             if (trackerListItem === undefined) {
839                                 trackerListItem = { host: host, trackerTorrentMap: new Map() };
840                                 trackerList.set(hash, trackerListItem);
841                             }
843                             trackerListItem.trackerTorrentMap.set(tracker, [...torrents]);
844                         }
845                         updateTrackers = true;
846                     }
847                     if (response["trackers_removed"]) {
848                         for (let i = 0; i < response["trackers_removed"].length; ++i) {
849                             const tracker = response["trackers_removed"][i];
850                             const hash = window.qBittorrent.Client.genHash(getHost(tracker));
851                             const trackerListEntry = trackerList.get(hash);
852                             if (trackerListEntry)
853                                 trackerListEntry.trackerTorrentMap.delete(tracker);
854                         }
855                         updateTrackers = true;
856                     }
857                     if (response["torrents"]) {
858                         let updateTorrentList = false;
859                         for (const key in response["torrents"]) {
860                             if (!Object.hasOwn(response["torrents"], key))
861                                 continue;
863                             response["torrents"][key]["hash"] = key;
864                             response["torrents"][key]["rowId"] = key;
865                             if (response["torrents"][key]["state"])
866                                 response["torrents"][key]["status"] = response["torrents"][key]["state"];
867                             torrentsTable.updateRowData(response["torrents"][key]);
868                             if (addTorrentToCategoryList(response["torrents"][key]))
869                                 update_categories = true;
870                             if (addTorrentToTagList(response["torrents"][key]))
871                                 updateTags = true;
872                             if (response["torrents"][key]["name"])
873                                 updateTorrentList = true;
874                         }
876                         if (updateTorrentList)
877                             setupCopyEventHandler();
878                     }
879                     if (response["torrents_removed"]) {
880                         response["torrents_removed"].each((hash) => {
881                             torrentsTable.removeRow(hash);
882                             removeTorrentFromCategoryList(hash);
883                             update_categories = true; // Always to update All category
884                             removeTorrentFromTagList(hash);
885                             updateTags = true; // Always to update All tag
886                         });
887                     }
888                     torrentsTable.updateTable(full_update);
889                     if (response["server_state"]) {
890                         const tmp = response["server_state"];
891                         for (const k in tmp) {
892                             if (!Object.hasOwn(tmp, k))
893                                 continue;
894                             serverState[k] = tmp[k];
895                         }
896                         processServerState();
897                     }
898                     updateFiltersList();
899                     if (update_categories) {
900                         updateCategoryList();
901                         window.qBittorrent.TransferList.contextMenu.updateCategoriesSubMenu(category_list);
902                     }
903                     if (updateTags) {
904                         updateTagList();
905                         window.qBittorrent.TransferList.contextMenu.updateTagsSubMenu(tagList);
906                     }
907                     if (updateTrackers)
908                         updateTrackerList();
910                     if (full_update)
911                         // re-select previously selected rows
912                         torrentsTable.reselectRows(torrentsTableSelectedRows);
913                 }
914                 syncRequestInProgress = false;
915                 syncData(window.qBittorrent.Client.getSyncMainDataInterval());
916             }
917         });
918         syncRequestInProgress = true;
919         request.send();
920     };
922     updateMainData = function() {
923         torrentsTable.updateTable();
924         syncData(100);
925     };
927     const syncData = function(delay) {
928         if (syncRequestInProgress)
929             return;
931         clearTimeout(syncMainDataTimeoutID);
932         syncMainDataTimeoutID = -1;
934         if (window.qBittorrent.Client.isStopped())
935             return;
937         syncMainDataTimeoutID = syncMainData.delay(delay);
938     };
940     const processServerState = function() {
941         let transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true);
942         if (serverState.dl_rate_limit > 0)
943             transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_rate_limit, true) + "]";
944         transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_data, false) + ")";
945         $("DlInfos").textContent = transfer_info;
946         transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true);
947         if (serverState.up_rate_limit > 0)
948             transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.up_rate_limit, true) + "]";
949         transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.up_info_data, false) + ")";
950         $("UpInfos").textContent = transfer_info;
952         document.title = (speedInTitle
953                 ? (`QBT_TR([D: %1, U: %2])QBT_TR[CONTEXT=MainWindow] `
954                     .replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true))
955                     .replace("%2", window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true)))
956                 : "")
957             + window.qBittorrent.Client.mainTitle();
959         $("freeSpaceOnDisk").textContent = "QBT_TR(Free space: %1)QBT_TR[CONTEXT=HttpServer]".replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.free_space_on_disk));
960         $("DHTNodes").textContent = "QBT_TR(DHT: %1 nodes)QBT_TR[CONTEXT=StatusBar]".replace("%1", serverState.dht_nodes);
962         // Statistics dialog
963         if (document.getElementById("statisticsContent")) {
964             $("AlltimeDL").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.alltime_dl, false);
965             $("AlltimeUL").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.alltime_ul, false);
966             $("TotalWastedSession").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_wasted_session, false);
967             $("GlobalRatio").textContent = serverState.global_ratio;
968             $("TotalPeerConnections").textContent = serverState.total_peer_connections;
969             $("ReadCacheHits").textContent = serverState.read_cache_hits + "%";
970             $("TotalBuffersSize").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_buffers_size, false);
971             $("WriteCacheOverload").textContent = serverState.write_cache_overload + "%";
972             $("ReadCacheOverload").textContent = serverState.read_cache_overload + "%";
973             $("QueuedIOJobs").textContent = serverState.queued_io_jobs;
974             $("AverageTimeInQueue").textContent = serverState.average_time_queue + " ms";
975             $("TotalQueuedSize").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_queued_size, false);
976         }
978         switch (serverState.connection_status) {
979             case "connected":
980                 $("connectionStatus").src = "images/connected.svg";
981                 $("connectionStatus").alt = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
982                 $("connectionStatus").title = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
983                 break;
984             case "firewalled":
985                 $("connectionStatus").src = "images/firewalled.svg";
986                 $("connectionStatus").alt = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
987                 $("connectionStatus").title = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
988                 break;
989             default:
990                 $("connectionStatus").src = "images/disconnected.svg";
991                 $("connectionStatus").alt = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
992                 $("connectionStatus").title = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
993                 break;
994         }
996         if (queueing_enabled !== serverState.queueing) {
997             queueing_enabled = serverState.queueing;
998             torrentsTable.columns["priority"].force_hide = !queueing_enabled;
999             torrentsTable.updateColumn("priority");
1000             if (queueing_enabled) {
1001                 $("topQueuePosItem").removeClass("invisible");
1002                 $("increaseQueuePosItem").removeClass("invisible");
1003                 $("decreaseQueuePosItem").removeClass("invisible");
1004                 $("bottomQueuePosItem").removeClass("invisible");
1005                 $("queueingButtons").removeClass("invisible");
1006                 $("queueingMenuItems").removeClass("invisible");
1007             }
1008             else {
1009                 $("topQueuePosItem").addClass("invisible");
1010                 $("increaseQueuePosItem").addClass("invisible");
1011                 $("decreaseQueuePosItem").addClass("invisible");
1012                 $("bottomQueuePosItem").addClass("invisible");
1013                 $("queueingButtons").addClass("invisible");
1014                 $("queueingMenuItems").addClass("invisible");
1015             }
1016         }
1018         if (alternativeSpeedLimits !== serverState.use_alt_speed_limits) {
1019             alternativeSpeedLimits = serverState.use_alt_speed_limits;
1020             updateAltSpeedIcon(alternativeSpeedLimits);
1021         }
1023         if (useSubcategories !== serverState.use_subcategories) {
1024             useSubcategories = serverState.use_subcategories;
1025             updateCategoryList();
1026         }
1028         serverSyncMainDataInterval = Math.max(serverState.refresh_interval, 500);
1029     };
1031     const updateAltSpeedIcon = function(enabled) {
1032         if (enabled) {
1033             $("alternativeSpeedLimits").src = "images/slow.svg";
1034             $("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
1035             $("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
1036         }
1037         else {
1038             $("alternativeSpeedLimits").src = "images/slow_off.svg";
1039             $("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
1040             $("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
1041         }
1042     };
1044     $("alternativeSpeedLimits").addEventListener("click", () => {
1045         // Change icon immediately to give some feedback
1046         updateAltSpeedIcon(!alternativeSpeedLimits);
1048         new Request({
1049             url: "api/v2/transfer/toggleSpeedLimitsMode",
1050             method: "post",
1051             onComplete: function() {
1052                 alternativeSpeedLimits = !alternativeSpeedLimits;
1053                 updateMainData();
1054             },
1055             onFailure: function() {
1056                 // Restore icon in case of failure
1057                 updateAltSpeedIcon(alternativeSpeedLimits);
1058             }
1059         }).send();
1060     });
1062     $("DlInfos").addEventListener("click", globalDownloadLimitFN);
1063     $("UpInfos").addEventListener("click", globalUploadLimitFN);
1065     $("showTopToolbarLink").addEventListener("click", (e) => {
1066         showTopToolbar = !showTopToolbar;
1067         LocalPreferences.set("show_top_toolbar", showTopToolbar.toString());
1068         if (showTopToolbar) {
1069             $("showTopToolbarLink").firstChild.style.opacity = "1";
1070             $("mochaToolbar").removeClass("invisible");
1071         }
1072         else {
1073             $("showTopToolbarLink").firstChild.style.opacity = "0";
1074             $("mochaToolbar").addClass("invisible");
1075         }
1076         MochaUI.Desktop.setDesktopSize();
1077     });
1079     $("showStatusBarLink").addEventListener("click", (e) => {
1080         showStatusBar = !showStatusBar;
1081         LocalPreferences.set("show_status_bar", showStatusBar.toString());
1082         if (showStatusBar) {
1083             $("showStatusBarLink").firstChild.style.opacity = "1";
1084             $("desktopFooterWrapper").removeClass("invisible");
1085         }
1086         else {
1087             $("showStatusBarLink").firstChild.style.opacity = "0";
1088             $("desktopFooterWrapper").addClass("invisible");
1089         }
1090         MochaUI.Desktop.setDesktopSize();
1091     });
1093     const registerMagnetHandler = function() {
1094         if (typeof navigator.registerProtocolHandler !== "function") {
1095             if (window.location.protocol !== "https:")
1096                 alert("QBT_TR(To use this feature, the WebUI needs to be accessed over HTTPS)QBT_TR[CONTEXT=MainWindow]");
1097             else
1098                 alert("QBT_TR(Your browser does not support this feature)QBT_TR[CONTEXT=MainWindow]");
1099             return;
1100         }
1102         const hashString = location.hash ? location.hash.replace(/^#/, "") : "";
1103         const hashParams = new URLSearchParams(hashString);
1104         hashParams.set("download", "");
1106         const templateHashString = hashParams.toString().replace("download=", "download=%s");
1107         const templateUrl = location.origin + location.pathname
1108             + location.search + "#" + templateHashString;
1110         navigator.registerProtocolHandler("magnet", templateUrl,
1111             "qBittorrent WebUI magnet handler");
1112     };
1113     $("registerMagnetHandlerLink").addEventListener("click", (e) => {
1114         registerMagnetHandler();
1115     });
1117     $("showFiltersSidebarLink").addEventListener("click", (e) => {
1118         showFiltersSidebar = !showFiltersSidebar;
1119         LocalPreferences.set("show_filters_sidebar", showFiltersSidebar.toString());
1120         if (showFiltersSidebar) {
1121             $("showFiltersSidebarLink").firstChild.style.opacity = "1";
1122             $("filtersColumn").removeClass("invisible");
1123             $("filtersColumn_handle").removeClass("invisible");
1124         }
1125         else {
1126             $("showFiltersSidebarLink").firstChild.style.opacity = "0";
1127             $("filtersColumn").addClass("invisible");
1128             $("filtersColumn_handle").addClass("invisible");
1129         }
1130         MochaUI.Desktop.setDesktopSize();
1131     });
1133     $("speedInBrowserTitleBarLink").addEventListener("click", (e) => {
1134         speedInTitle = !speedInTitle;
1135         LocalPreferences.set("speed_in_browser_title_bar", speedInTitle.toString());
1136         if (speedInTitle)
1137             $("speedInBrowserTitleBarLink").firstChild.style.opacity = "1";
1138         else
1139             $("speedInBrowserTitleBarLink").firstChild.style.opacity = "0";
1140         processServerState();
1141     });
1143     $("showSearchEngineLink").addEventListener("click", (e) => {
1144         window.qBittorrent.Client.showSearchEngine(!window.qBittorrent.Client.isShowSearchEngine());
1145         LocalPreferences.set("show_search_engine", window.qBittorrent.Client.isShowSearchEngine().toString());
1146         updateTabDisplay();
1147     });
1149     $("showRssReaderLink").addEventListener("click", (e) => {
1150         window.qBittorrent.Client.showRssReader(!window.qBittorrent.Client.isShowRssReader());
1151         LocalPreferences.set("show_rss_reader", window.qBittorrent.Client.isShowRssReader().toString());
1152         updateTabDisplay();
1153     });
1155     $("showLogViewerLink").addEventListener("click", (e) => {
1156         window.qBittorrent.Client.showLogViewer(!window.qBittorrent.Client.isShowLogViewer());
1157         LocalPreferences.set("show_log_viewer", window.qBittorrent.Client.isShowLogViewer().toString());
1158         updateTabDisplay();
1159     });
1161     const updateTabDisplay = function() {
1162         if (window.qBittorrent.Client.isShowRssReader()) {
1163             $("showRssReaderLink").firstChild.style.opacity = "1";
1164             $("mainWindowTabs").removeClass("invisible");
1165             $("rssTabLink").removeClass("invisible");
1166             if (!MochaUI.Panels.instances.RssPanel)
1167                 addRssPanel();
1168         }
1169         else {
1170             $("showRssReaderLink").firstChild.style.opacity = "0";
1171             $("rssTabLink").addClass("invisible");
1172             if ($("rssTabLink").hasClass("selected"))
1173                 $("transfersTabLink").click();
1174         }
1176         if (window.qBittorrent.Client.isShowSearchEngine()) {
1177             $("showSearchEngineLink").firstChild.style.opacity = "1";
1178             $("mainWindowTabs").removeClass("invisible");
1179             $("searchTabLink").removeClass("invisible");
1180             if (!MochaUI.Panels.instances.SearchPanel)
1181                 addSearchPanel();
1182         }
1183         else {
1184             $("showSearchEngineLink").firstChild.style.opacity = "0";
1185             $("searchTabLink").addClass("invisible");
1186             if ($("searchTabLink").hasClass("selected"))
1187                 $("transfersTabLink").click();
1188         }
1190         if (window.qBittorrent.Client.isShowLogViewer()) {
1191             $("showLogViewerLink").firstChild.style.opacity = "1";
1192             $("mainWindowTabs").removeClass("invisible");
1193             $("logTabLink").removeClass("invisible");
1194             if (!MochaUI.Panels.instances.LogPanel)
1195                 addLogPanel();
1196         }
1197         else {
1198             $("showLogViewerLink").firstChild.style.opacity = "0";
1199             $("logTabLink").addClass("invisible");
1200             if ($("logTabLink").hasClass("selected"))
1201                 $("transfersTabLink").click();
1202         }
1204         // display no tabs
1205         if (!window.qBittorrent.Client.isShowRssReader() && !window.qBittorrent.Client.isShowSearchEngine() && !window.qBittorrent.Client.isShowLogViewer())
1206             $("mainWindowTabs").addClass("invisible");
1207     };
1209     $("StatisticsLink").addEventListener("click", StatisticsLinkFN);
1211     // main window tabs
1213     const showTransfersTab = function() {
1214         const showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
1215         if (showFiltersSidebar) {
1216             $("filtersColumn").removeClass("invisible");
1217             $("filtersColumn_handle").removeClass("invisible");
1218         }
1219         $("mainColumn").removeClass("invisible");
1220         $("torrentsFilterToolbar").removeClass("invisible");
1222         customSyncMainDataInterval = null;
1223         syncData(100);
1225         hideSearchTab();
1226         hideRssTab();
1227         hideLogTab();
1229         LocalPreferences.set("selected_window_tab", "transfers");
1230     };
1232     const hideTransfersTab = function() {
1233         $("filtersColumn").addClass("invisible");
1234         $("filtersColumn_handle").addClass("invisible");
1235         $("mainColumn").addClass("invisible");
1236         $("torrentsFilterToolbar").addClass("invisible");
1237         MochaUI.Desktop.resizePanels();
1238     };
1240     const showSearchTab = (function() {
1241         let searchTabInitialized = false;
1243         return () => {
1244             // we must wait until the panel is fully loaded before proceeding.
1245             // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1246             // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1247             if (!isSearchPanelLoaded) {
1248                 setTimeout(() => {
1249                     showSearchTab();
1250                 }, 100);
1251                 return;
1252             }
1254             if (!searchTabInitialized) {
1255                 window.qBittorrent.Search.init();
1256                 searchTabInitialized = true;
1257             }
1259             $("searchTabColumn").removeClass("invisible");
1260             customSyncMainDataInterval = 30000;
1261             hideTransfersTab();
1262             hideRssTab();
1263             hideLogTab();
1265             LocalPreferences.set("selected_window_tab", "search");
1266         };
1267     })();
1269     const hideSearchTab = function() {
1270         $("searchTabColumn").addClass("invisible");
1271         MochaUI.Desktop.resizePanels();
1272     };
1274     const showRssTab = (function() {
1275         let rssTabInitialized = false;
1277         return () => {
1278             if (!rssTabInitialized) {
1279                 window.qBittorrent.Rss.init();
1280                 rssTabInitialized = true;
1281             }
1282             else {
1283                 window.qBittorrent.Rss.load();
1284             }
1286             $("rssTabColumn").removeClass("invisible");
1287             customSyncMainDataInterval = 30000;
1288             hideTransfersTab();
1289             hideSearchTab();
1290             hideLogTab();
1292             LocalPreferences.set("selected_window_tab", "rss");
1293         };
1294     })();
1296     const hideRssTab = function() {
1297         $("rssTabColumn").addClass("invisible");
1298         window.qBittorrent.Rss && window.qBittorrent.Rss.unload();
1299         MochaUI.Desktop.resizePanels();
1300     };
1302     const showLogTab = (function() {
1303         let logTabInitialized = false;
1305         return () => {
1306             // we must wait until the panel is fully loaded before proceeding.
1307             // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1308             // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1309             if (!isLogPanelLoaded) {
1310                 setTimeout(() => {
1311                     showLogTab();
1312                 }, 100);
1313                 return;
1314             }
1316             if (!logTabInitialized) {
1317                 window.qBittorrent.Log.init();
1318                 logTabInitialized = true;
1319             }
1320             else {
1321                 window.qBittorrent.Log.load();
1322             }
1324             $("logTabColumn").removeClass("invisible");
1325             customSyncMainDataInterval = 30000;
1326             hideTransfersTab();
1327             hideSearchTab();
1328             hideRssTab();
1330             LocalPreferences.set("selected_window_tab", "log");
1331         };
1332     })();
1334     const hideLogTab = function() {
1335         $("logTabColumn").addClass("invisible");
1336         MochaUI.Desktop.resizePanels();
1337         window.qBittorrent.Log && window.qBittorrent.Log.unload();
1338     };
1340     const addSearchPanel = function() {
1341         new MochaUI.Panel({
1342             id: "SearchPanel",
1343             title: "Search",
1344             header: false,
1345             padding: {
1346                 top: 0,
1347                 right: 0,
1348                 bottom: 0,
1349                 left: 0
1350             },
1351             loadMethod: "xhr",
1352             contentURL: "views/search.html",
1353             require: {
1354                 js: ["scripts/search.js"],
1355                 onload: () => {
1356                     isSearchPanelLoaded = true;
1357                 },
1358             },
1359             content: "",
1360             column: "searchTabColumn",
1361             height: null
1362         });
1363     };
1365     const addRssPanel = function() {
1366         new MochaUI.Panel({
1367             id: "RssPanel",
1368             title: "Rss",
1369             header: false,
1370             padding: {
1371                 top: 0,
1372                 right: 0,
1373                 bottom: 0,
1374                 left: 0
1375             },
1376             loadMethod: "xhr",
1377             contentURL: "views/rss.html",
1378             content: "",
1379             column: "rssTabColumn",
1380             height: null
1381         });
1382     };
1384     const addLogPanel = function() {
1385         new MochaUI.Panel({
1386             id: "LogPanel",
1387             title: "Log",
1388             header: true,
1389             padding: {
1390                 top: 0,
1391                 right: 0,
1392                 bottom: 0,
1393                 left: 0
1394             },
1395             loadMethod: "xhr",
1396             contentURL: "views/log.html",
1397             require: {
1398                 css: ["css/vanillaSelectBox.css"],
1399                 js: ["scripts/lib/vanillaSelectBox.js"],
1400                 onload: () => {
1401                     isLogPanelLoaded = true;
1402                 },
1403             },
1404             tabsURL: "views/logTabs.html",
1405             tabsOnload: function() {
1406                 MochaUI.initializeTabs("panelTabs");
1408                 $("logMessageLink").addEventListener("click", (e) => {
1409                     window.qBittorrent.Log.setCurrentTab("main");
1410                 });
1412                 $("logPeerLink").addEventListener("click", (e) => {
1413                     window.qBittorrent.Log.setCurrentTab("peer");
1414                 });
1415             },
1416             collapsible: false,
1417             content: "",
1418             column: "logTabColumn",
1419             height: null
1420         });
1421     };
1423     const handleDownloadParam = function() {
1424         // Extract torrent URL from download param in WebUI URL hash
1425         const downloadHash = "#download=";
1426         if (location.hash.indexOf(downloadHash) !== 0)
1427             return;
1429         const url = decodeURIComponent(location.hash.substring(downloadHash.length));
1430         // Remove the processed hash from the URL
1431         history.replaceState("", document.title, (location.pathname + location.search));
1432         showDownloadPage([url]);
1433     };
1435     new MochaUI.Panel({
1436         id: "transferList",
1437         title: "Panel",
1438         header: false,
1439         padding: {
1440             top: 0,
1441             right: 0,
1442             bottom: 0,
1443             left: 0
1444         },
1445         loadMethod: "xhr",
1446         contentURL: "views/transferlist.html",
1447         onContentLoaded: function() {
1448             handleDownloadParam();
1449             updateMainData();
1450         },
1451         column: "mainColumn",
1452         onResize: saveColumnSizes,
1453         height: null
1454     });
1455     let prop_h = LocalPreferences.get("properties_height_rel");
1456     if (prop_h !== null)
1457         prop_h = prop_h.toFloat() * Window.getSize().y;
1458     else
1459         prop_h = Window.getSize().y / 2.0;
1460     new MochaUI.Panel({
1461         id: "propertiesPanel",
1462         title: "Panel",
1463         header: true,
1464         padding: {
1465             top: 0,
1466             right: 0,
1467             bottom: 0,
1468             left: 0
1469         },
1470         contentURL: "views/properties.html",
1471         require: {
1472             css: ["css/Tabs.css", "css/dynamicTable.css"],
1473             js: ["scripts/prop-general.js", "scripts/prop-trackers.js", "scripts/prop-peers.js", "scripts/prop-webseeds.js", "scripts/prop-files.js"],
1474         },
1475         tabsURL: "views/propertiesToolbar.html",
1476         tabsOnload: function() {
1477             MochaUI.initializeTabs("propertiesTabs");
1479             updatePropertiesPanel = function() {
1480                 if (!$("prop_general").hasClass("invisible")) {
1481                     if (window.qBittorrent.PropGeneral !== undefined)
1482                         window.qBittorrent.PropGeneral.updateData();
1483                 }
1484                 else if (!$("prop_trackers").hasClass("invisible")) {
1485                     if (window.qBittorrent.PropTrackers !== undefined)
1486                         window.qBittorrent.PropTrackers.updateData();
1487                 }
1488                 else if (!$("prop_peers").hasClass("invisible")) {
1489                     if (window.qBittorrent.PropPeers !== undefined)
1490                         window.qBittorrent.PropPeers.updateData();
1491                 }
1492                 else if (!$("prop_webseeds").hasClass("invisible")) {
1493                     if (window.qBittorrent.PropWebseeds !== undefined)
1494                         window.qBittorrent.PropWebseeds.updateData();
1495                 }
1496                 else if (!$("prop_files").hasClass("invisible")) {
1497                     if (window.qBittorrent.PropFiles !== undefined)
1498                         window.qBittorrent.PropFiles.updateData();
1499                 }
1500             };
1502             $("PropGeneralLink").addEventListener("click", function(e) {
1503                 $$(".propertiesTabContent").addClass("invisible");
1504                 $("prop_general").removeClass("invisible");
1505                 hideFilesFilter();
1506                 updatePropertiesPanel();
1507                 LocalPreferences.set("selected_tab", this.id);
1508             });
1510             $("PropTrackersLink").addEventListener("click", function(e) {
1511                 $$(".propertiesTabContent").addClass("invisible");
1512                 $("prop_trackers").removeClass("invisible");
1513                 hideFilesFilter();
1514                 updatePropertiesPanel();
1515                 LocalPreferences.set("selected_tab", this.id);
1516             });
1518             $("PropPeersLink").addEventListener("click", function(e) {
1519                 $$(".propertiesTabContent").addClass("invisible");
1520                 $("prop_peers").removeClass("invisible");
1521                 hideFilesFilter();
1522                 updatePropertiesPanel();
1523                 LocalPreferences.set("selected_tab", this.id);
1524             });
1526             $("PropWebSeedsLink").addEventListener("click", function(e) {
1527                 $$(".propertiesTabContent").addClass("invisible");
1528                 $("prop_webseeds").removeClass("invisible");
1529                 hideFilesFilter();
1530                 updatePropertiesPanel();
1531                 LocalPreferences.set("selected_tab", this.id);
1532             });
1534             $("PropFilesLink").addEventListener("click", function(e) {
1535                 $$(".propertiesTabContent").addClass("invisible");
1536                 $("prop_files").removeClass("invisible");
1537                 showFilesFilter();
1538                 updatePropertiesPanel();
1539                 LocalPreferences.set("selected_tab", this.id);
1540             });
1542             $("propertiesPanel_collapseToggle").addEventListener("click", (e) => {
1543                 updatePropertiesPanel();
1544             });
1545         },
1546         column: "mainColumn",
1547         height: prop_h
1548     });
1550     const showFilesFilter = function() {
1551         $("torrentFilesFilterToolbar").removeClass("invisible");
1552     };
1554     const hideFilesFilter = function() {
1555         $("torrentFilesFilterToolbar").addClass("invisible");
1556     };
1558     // listen for changes to torrentsFilterInput
1559     let torrentsFilterInputTimer = -1;
1560     $("torrentsFilterInput").addEventListener("input", () => {
1561         clearTimeout(torrentsFilterInputTimer);
1562         torrentsFilterInputTimer = setTimeout(() => {
1563             torrentsFilterInputTimer = -1;
1564             torrentsTable.updateTable();
1565         }, window.qBittorrent.Misc.FILTER_INPUT_DELAY);
1566     });
1568     document.getElementById("torrentsFilterToolbar").addEventListener("change", (e) => { torrentsTable.updateTable(); });
1570     $("transfersTabLink").addEventListener("click", showTransfersTab);
1571     $("searchTabLink").addEventListener("click", showSearchTab);
1572     $("rssTabLink").addEventListener("click", showRssTab);
1573     $("logTabLink").addEventListener("click", showLogTab);
1574     updateTabDisplay();
1576     const registerDragAndDrop = () => {
1577         $("desktop").addEventListener("dragover", (ev) => {
1578             if (ev.preventDefault)
1579                 ev.preventDefault();
1580         });
1582         $("desktop").addEventListener("dragenter", (ev) => {
1583             if (ev.preventDefault)
1584                 ev.preventDefault();
1585         });
1587         $("desktop").addEventListener("drop", (ev) => {
1588             if (ev.preventDefault)
1589                 ev.preventDefault();
1591             const droppedFiles = ev.dataTransfer.files;
1593             if (droppedFiles.length > 0) {
1594                 // dropped files or folders
1596                 // can't handle folder due to cannot put the filelist (from dropped folder)
1597                 // to <input> `files` field
1598                 for (const item of ev.dataTransfer.items) {
1599                     if (item.webkitGetAsEntry().isDirectory)
1600                         return;
1601                 }
1603                 const id = "uploadPage";
1604                 new MochaUI.Window({
1605                     id: id,
1606                     icon: "images/qbittorrent-tray.svg",
1607                     title: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
1608                     loadMethod: "iframe",
1609                     contentURL: new URI("upload.html").toString(),
1610                     addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
1611                     scrollbars: true,
1612                     maximizable: false,
1613                     paddingVertical: 0,
1614                     paddingHorizontal: 0,
1615                     width: loadWindowWidth(id, 500),
1616                     height: loadWindowHeight(id, 460),
1617                     onResize: () => {
1618                         saveWindowSize(id);
1619                     },
1620                     onContentLoaded: () => {
1621                         const fileInput = $(`${id}_iframe`).contentDocument.getElementById("fileselect");
1622                         fileInput.files = droppedFiles;
1623                     }
1624                 });
1625             }
1627             const droppedText = ev.dataTransfer.getData("text");
1628             if (droppedText.length > 0) {
1629                 // dropped text
1631                 const urls = droppedText.split("\n")
1632                     .map((str) => str.trim())
1633                     .filter((str) => {
1634                         const lowercaseStr = str.toLowerCase();
1635                         return lowercaseStr.startsWith("http:")
1636                             || lowercaseStr.startsWith("https:")
1637                             || lowercaseStr.startsWith("magnet:")
1638                             || ((str.length === 40) && !(/[^0-9A-F]/i.test(str))) // v1 hex-encoded SHA-1 info-hash
1639                             || ((str.length === 32) && !(/[^2-7A-Z]/i.test(str))); // v1 Base32 encoded SHA-1 info-hash
1640                     });
1642                 if (urls.length <= 0)
1643                     return;
1645                 const id = "downloadPage";
1646                 const contentURI = new URI("download.html").setData("urls", urls.map(encodeURIComponent).join("|"));
1647                 new MochaUI.Window({
1648                     id: id,
1649                     icon: "images/qbittorrent-tray.svg",
1650                     title: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
1651                     loadMethod: "iframe",
1652                     contentURL: contentURI.toString(),
1653                     addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
1654                     scrollbars: true,
1655                     maximizable: false,
1656                     closable: true,
1657                     paddingVertical: 0,
1658                     paddingHorizontal: 0,
1659                     width: loadWindowWidth(id, 500),
1660                     height: loadWindowHeight(id, 600),
1661                     onResize: () => {
1662                         saveWindowSize(id);
1663                     }
1664                 });
1665             }
1666         });
1667     };
1668     registerDragAndDrop();
1670     new Keyboard({
1671         defaultEventType: "keydown",
1672         events: {
1673             "ctrl+a": function(event) {
1674                 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1675                     return;
1676                 if (event.target.isContentEditable)
1677                     return;
1678                 torrentsTable.selectAll();
1679                 event.preventDefault();
1680             },
1681             "delete": function(event) {
1682                 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1683                     return;
1684                 if (event.target.isContentEditable)
1685                     return;
1686                 deleteFN();
1687                 event.preventDefault();
1688             },
1689             "shift+delete": (event) => {
1690                 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1691                     return;
1692                 if (event.target.isContentEditable)
1693                     return;
1694                 deleteFN(true);
1695                 event.preventDefault();
1696             }
1697         }
1698     }).activate();
1701 window.addEventListener("load", () => {
1702     // fetch various data and store it in memory
1703     window.qBittorrent.Cache.buildInfo.init();
1704     window.qBittorrent.Cache.preferences.init();
1705     window.qBittorrent.Cache.qbtVersion.init();
1707     // switch to previously used tab
1708     const previouslyUsedTab = LocalPreferences.get("selected_window_tab", "transfers");
1709     switch (previouslyUsedTab) {
1710         case "search":
1711             if (window.qBittorrent.Client.isShowSearchEngine())
1712                 $("searchTabLink").click();
1713             break;
1714         case "rss":
1715             if (window.qBittorrent.Client.isShowRssReader())
1716                 $("rssTabLink").click();
1717             break;
1718         case "log":
1719             if (window.qBittorrent.Client.isShowLogViewer())
1720                 $("logTabLink").click();
1721             break;
1722         case "transfers":
1723             $("transfersTabLink").click();
1724             break;
1725         default:
1726             console.error(`Unexpected 'selected_window_tab' value: ${previouslyUsedTab}`);
1727             $("transfersTabLink").click();
1728             break;
1729     };