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