Stop sync requests after qbt has been shutdown
[qBittorrent.git] / src / webui / www / private / scripts / client.js
blobed0fa08cbd05712f1c201fb0227d9f12df901863
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         };
41     };
43     const closeWindows = function() {
44         MochaUI.closeAll();
45     };
47     const genHash = function(string) {
48         // origins:
49         // https://stackoverflow.com/a/8831937
50         // https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0
51         let hash = 0;
52         for (let i = 0; i < string.length; ++i)
53             hash = ((Math.imul(hash, 31) + string.charCodeAt(i)) | 0);
54         return hash;
55     };
57     const getSyncMainDataInterval = function() {
58         return customSyncMainDataInterval ? customSyncMainDataInterval : serverSyncMainDataInterval;
59     };
61     let stopped = false;
62     const isStopped = () => {
63         return stopped;
64     };
66     const stop = () => {
67         stopped = true;
68     };
70     return exports();
71 })();
72 Object.freeze(window.qBittorrent.Client);
74 // TODO: move global functions/variables into some namespace/scope
76 this.torrentsTable = new window.qBittorrent.DynamicTable.TorrentsTable();
78 let updatePropertiesPanel = function() {};
80 this.updateMainData = function() {};
81 let alternativeSpeedLimits = false;
82 let queueing_enabled = true;
83 let serverSyncMainDataInterval = 1500;
84 let customSyncMainDataInterval = null;
85 let useSubcategories = true;
87 /* Categories filter */
88 const CATEGORIES_ALL = 1;
89 const CATEGORIES_UNCATEGORIZED = 2;
91 const category_list = new Map();
93 let selected_category = Number(LocalPreferences.get('selected_category', CATEGORIES_ALL));
94 let setCategoryFilter = function() {};
96 /* Tags filter */
97 const TAGS_ALL = 1;
98 const TAGS_UNTAGGED = 2;
100 const tagList = new Map();
102 let selectedTag = Number(LocalPreferences.get('selected_tag', TAGS_ALL));
103 let setTagFilter = function() {};
105 /* Trackers filter */
106 const TRACKERS_ALL = 1;
107 const TRACKERS_TRACKERLESS = 2;
109 const trackerList = new Map();
111 let selectedTracker = LocalPreferences.get('selected_tracker', TRACKERS_ALL);
112 let setTrackerFilter = function() {};
114 /* All filters */
115 let selected_filter = LocalPreferences.get('selected_filter', 'all');
116 let setFilter = function() {};
117 let toggleFilterDisplay = function() {};
119 window.addEventListener("DOMContentLoaded", function() {
120     const saveColumnSizes = function() {
121         const filters_width = $('Filters').getSize().x;
122         const properties_height_rel = $('propertiesPanel').getSize().y / Window.getSize().y;
123         LocalPreferences.set('filters_width', filters_width);
124         LocalPreferences.set('properties_height_rel', properties_height_rel);
125     };
127     window.addEvent('resize', function() {
128         // only save sizes if the columns are visible
129         if (!$("mainColumn").hasClass("invisible"))
130             saveColumnSizes.delay(200); // Resizing might takes some time.
131     });
133     /*MochaUI.Desktop = new MochaUI.Desktop();
134     MochaUI.Desktop.desktop.setStyles({
135         'background': '#fff',
136         'visibility': 'visible'
137     });*/
138     MochaUI.Desktop.initialize();
140     const buildTransfersTab = function() {
141         let filt_w = LocalPreferences.get('filters_width');
142         if ($defined(filt_w))
143             filt_w = filt_w.toInt();
144         else
145             filt_w = 120;
146         new MochaUI.Column({
147             id: 'filtersColumn',
148             placement: 'left',
149             onResize: saveColumnSizes,
150             width: filt_w,
151             resizeLimit: [1, 300]
152         });
154         new MochaUI.Column({
155             id: 'mainColumn',
156             placement: 'main'
157         });
158     };
160     const buildSearchTab = function() {
161         new MochaUI.Column({
162             id: 'searchTabColumn',
163             placement: 'main',
164             width: null
165         });
167         // start off hidden
168         $("searchTabColumn").addClass("invisible");
169     };
171     const buildRssTab = function() {
172         new MochaUI.Column({
173             id: 'rssTabColumn',
174             placement: 'main',
175             width: null
176         });
178         // start off hidden
179         $("rssTabColumn").addClass("invisible");
180     };
182     const buildLogTab = function() {
183         new MochaUI.Column({
184             id: 'logTabColumn',
185             placement: 'main',
186             width: null
187         });
189         // start off hidden
190         $('logTabColumn').addClass('invisible');
191     };
193     buildTransfersTab();
194     buildSearchTab();
195     buildRssTab();
196     buildLogTab();
197     MochaUI.initializeTabs('mainWindowTabsList');
199     setCategoryFilter = function(hash) {
200         selected_category = hash;
201         LocalPreferences.set('selected_category', selected_category);
202         highlightSelectedCategory();
203         if (typeof torrentsTable.tableBody != 'undefined')
204             updateMainData();
205     };
207     setTagFilter = function(hash) {
208         selectedTag = hash;
209         LocalPreferences.set('selected_tag', selectedTag);
210         highlightSelectedTag();
211         if (torrentsTable.tableBody !== undefined)
212             updateMainData();
213     };
215     setTrackerFilter = function(hash) {
216         selectedTracker = hash.toString();
217         LocalPreferences.set('selected_tracker', selectedTracker);
218         highlightSelectedTracker();
219         if (torrentsTable.tableBody !== undefined)
220             updateMainData();
221     };
223     setFilter = function(f) {
224         // Visually Select the right filter
225         $("all_filter").removeClass("selectedFilter");
226         $("downloading_filter").removeClass("selectedFilter");
227         $("seeding_filter").removeClass("selectedFilter");
228         $("completed_filter").removeClass("selectedFilter");
229         $("paused_filter").removeClass("selectedFilter");
230         $("resumed_filter").removeClass("selectedFilter");
231         $("active_filter").removeClass("selectedFilter");
232         $("inactive_filter").removeClass("selectedFilter");
233         $("stalled_filter").removeClass("selectedFilter");
234         $("stalled_uploading_filter").removeClass("selectedFilter");
235         $("stalled_downloading_filter").removeClass("selectedFilter");
236         $("checking_filter").removeClass("selectedFilter");
237         $("moving_filter").removeClass("selectedFilter");
238         $("errored_filter").removeClass("selectedFilter");
239         $(f + "_filter").addClass("selectedFilter");
240         selected_filter = f;
241         LocalPreferences.set('selected_filter', f);
242         // Reload torrents
243         if (typeof torrentsTable.tableBody != 'undefined')
244             updateMainData();
245     };
247     toggleFilterDisplay = function(filter) {
248         const element = filter + "FilterList";
249         LocalPreferences.set('filter_' + filter + "_collapsed", !$(element).hasClass("invisible"));
250         $(element).toggleClass("invisible");
251         const parent = $(element).getParent(".filterWrapper");
252         const toggleIcon = $(parent).getChildren(".filterTitle img");
253         if (toggleIcon)
254             toggleIcon[0].toggleClass("rotate");
255     };
257     new MochaUI.Panel({
258         id: 'Filters',
259         title: 'Panel',
260         header: false,
261         padding: {
262             top: 0,
263             right: 0,
264             bottom: 0,
265             left: 0
266         },
267         loadMethod: 'xhr',
268         contentURL: 'views/filters.html',
269         onContentLoaded: function() {
270             setFilter(selected_filter);
271         },
272         column: 'filtersColumn',
273         height: 300
274     });
275     initializeWindows();
277     // Show Top Toolbar is enabled by default
278     let showTopToolbar = LocalPreferences.get('show_top_toolbar', 'true') == "true";
279     if (!showTopToolbar) {
280         $('showTopToolbarLink').firstChild.style.opacity = '0';
281         $('mochaToolbar').addClass('invisible');
282     }
284     // Show Status Bar is enabled by default
285     let showStatusBar = LocalPreferences.get('show_status_bar', 'true') === "true";
286     if (!showStatusBar) {
287         $('showStatusBarLink').firstChild.style.opacity = '0';
288         $('desktopFooterWrapper').addClass('invisible');
289     }
291     // Show Filters Sidebar is enabled by default
292     let showFiltersSidebar = LocalPreferences.get('show_filters_sidebar', 'true') === "true";
293     if (!showFiltersSidebar) {
294         $('showFiltersSidebarLink').firstChild.style.opacity = '0';
295         $('filtersColumn').addClass('invisible');
296         $('filtersColumn_handle').addClass('invisible');
297     }
299     let speedInTitle = LocalPreferences.get('speed_in_browser_title_bar') == "true";
300     if (!speedInTitle)
301         $('speedInBrowserTitleBarLink').firstChild.style.opacity = '0';
303     // After showing/hiding the toolbar + status bar
304     let showSearchEngine = LocalPreferences.get('show_search_engine') !== "false";
305     let showRssReader = LocalPreferences.get('show_rss_reader') !== "false";
306     let showLogViewer = LocalPreferences.get('show_log_viewer') === 'true';
308     // After Show Top Toolbar
309     MochaUI.Desktop.setDesktopSize();
311     let syncMainDataLastResponseId = 0;
312     const serverState = {};
314     const removeTorrentFromCategoryList = function(hash) {
315         if (!hash)
316             return false;
318         let removed = false;
319         category_list.forEach((category) => {
320             const deleteResult = category.torrents.delete(hash);
321             removed ||= deleteResult;
322         });
324         return removed;
325     };
327     const addTorrentToCategoryList = function(torrent) {
328         const category = torrent['category'];
329         if (typeof category === 'undefined')
330             return false;
332         const hash = torrent['hash'];
333         if (category.length === 0) { // Empty category
334             removeTorrentFromCategoryList(hash);
335             return true;
336         }
338         const categoryHash = window.qBittorrent.Client.genHash(category);
339         if (!category_list.has(categoryHash)) { // This should not happen
340             category_list.set(categoryHash, {
341                 name: category,
342                 torrents: new Set()
343             });
344         }
346         const torrents = category_list.get(categoryHash).torrents;
347         if (!torrents.has(hash)) {
348             removeTorrentFromCategoryList(hash);
349             torrents.add(hash);
350             return true;
351         }
352         return false;
353     };
355     const removeTorrentFromTagList = function(hash) {
356         if (!hash)
357             return false;
359         let removed = false;
360         tagList.forEach((tag) => {
361             const deleteResult = tag.torrents.delete(hash);
362             removed ||= deleteResult;
363         });
365         return removed;
366     };
368     const addTorrentToTagList = function(torrent) {
369         if (torrent['tags'] === undefined) // Tags haven't changed
370             return false;
372         const hash = torrent['hash'];
373         removeTorrentFromTagList(hash);
375         if (torrent['tags'].length === 0) // No tags
376             return true;
378         const tags = torrent['tags'].split(',');
379         let added = false;
380         for (let i = 0; i < tags.length; ++i) {
381             const tagHash = window.qBittorrent.Client.genHash(tags[i].trim());
382             if (!tagList.has(tagHash)) { // This should not happen
383                 tagList.set(tagHash, {
384                     name: tags,
385                     torrents: new Set()
386                 });
387             }
389             const torrents = tagList.get(tagHash).torrents;
390             if (!torrents.has(hash)) {
391                 torrents.add(hash);
392                 added = true;
393             }
394         }
395         return added;
396     };
398     const updateFilter = function(filter, filterTitle) {
399         $(filter + '_filter').firstChild.childNodes[1].nodeValue = filterTitle.replace('%1', torrentsTable.getFilteredTorrentsNumber(filter, CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL));
400     };
402     const updateFiltersList = function() {
403         updateFilter('all', 'QBT_TR(All (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
404         updateFilter('downloading', 'QBT_TR(Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
405         updateFilter('seeding', 'QBT_TR(Seeding (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
406         updateFilter('completed', 'QBT_TR(Completed (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
407         updateFilter('resumed', 'QBT_TR(Resumed (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
408         updateFilter('paused', 'QBT_TR(Paused (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
409         updateFilter('active', 'QBT_TR(Active (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
410         updateFilter('inactive', 'QBT_TR(Inactive (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
411         updateFilter('stalled', 'QBT_TR(Stalled (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
412         updateFilter('stalled_uploading', 'QBT_TR(Stalled Uploading (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
413         updateFilter('stalled_downloading', 'QBT_TR(Stalled Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
414         updateFilter('checking', 'QBT_TR(Checking (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
415         updateFilter('moving', 'QBT_TR(Moving (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
416         updateFilter('errored', 'QBT_TR(Errored (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
417     };
419     const updateCategoryList = function() {
420         const categoryList = $('categoryFilterList');
421         if (!categoryList)
422             return;
423         categoryList.getChildren().each(c => c.destroy());
425         const create_link = function(hash, text, count) {
426             let display_name = text;
427             let margin_left = 0;
428             if (useSubcategories) {
429                 const category_path = text.split("/");
430                 display_name = category_path[category_path.length - 1];
431                 margin_left = (category_path.length - 1) * 20;
432             }
434             const html = `<a href="#" style="margin-left: ${margin_left}px;" onclick="setCategoryFilter(${hash}); return false;">`
435                 + '<img src="images/view-categories.svg"/>'
436                 + window.qBittorrent.Misc.escapeHtml(display_name) + ' (' + count + ')' + '</a>';
437             const el = new Element('li', {
438                 id: hash,
439                 html: html
440             });
441             window.qBittorrent.Filters.categoriesFilterContextMenu.addTarget(el);
442             return el;
443         };
445         const all = torrentsTable.getRowIds().length;
446         let uncategorized = 0;
447         for (const key in torrentsTable.rows) {
448             if (!Object.hasOwn(torrentsTable.rows, key))
449                 continue;
451             const row = torrentsTable.rows[key];
452             if (row['full_data'].category.length === 0)
453                 uncategorized += 1;
454         }
455         categoryList.appendChild(create_link(CATEGORIES_ALL, 'QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]', all));
456         categoryList.appendChild(create_link(CATEGORIES_UNCATEGORIZED, 'QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]', uncategorized));
458         const sortedCategories = [];
459         category_list.forEach((category, hash) => sortedCategories.push({
460             categoryName: category.name,
461             categoryHash: hash,
462             categoryCount: category.torrents.size
463         }));
464         sortedCategories.sort((left, right) => {
465             const leftSegments = left.categoryName.split('/');
466             const rightSegments = right.categoryName.split('/');
468             for (let i = 0, iMax = Math.min(leftSegments.length, rightSegments.length); i < iMax; ++i) {
469                 const compareResult = window.qBittorrent.Misc.naturalSortCollator.compare(
470                     leftSegments[i], rightSegments[i]);
471                 if (compareResult !== 0)
472                     return compareResult;
473             }
475             return leftSegments.length - rightSegments.length;
476         });
478         for (let i = 0; i < sortedCategories.length; ++i) {
479             const { categoryName, categoryHash } = sortedCategories[i];
480             let { categoryCount } = sortedCategories[i];
482             if (useSubcategories) {
483                 for (let j = (i + 1);
484                     ((j < sortedCategories.length) && sortedCategories[j].categoryName.startsWith(categoryName + "/")); ++j) {
485                     categoryCount += sortedCategories[j].categoryCount;
486                 }
487             }
489             categoryList.appendChild(create_link(categoryHash, categoryName, categoryCount));
490         }
492         highlightSelectedCategory();
493     };
495     const highlightSelectedCategory = function() {
496         const categoryList = $('categoryFilterList');
497         if (!categoryList)
498             return;
499         const children = categoryList.childNodes;
500         for (let i = 0; i < children.length; ++i) {
501             if (Number(children[i].id) === selected_category)
502                 children[i].className = "selectedFilter";
503             else
504                 children[i].className = "";
505         }
506     };
508     const updateTagList = function() {
509         const tagFilterList = $('tagFilterList');
510         if (tagFilterList === null)
511             return;
513         tagFilterList.getChildren().each(c => c.destroy());
515         const createLink = function(hash, text, count) {
516             const html = `<a href="#" onclick="setTagFilter(${hash}); return false;">`
517                 + '<img src="images/tags.svg"/>'
518                 + window.qBittorrent.Misc.escapeHtml(text) + ' (' + count + ')' + '</a>';
519             const el = new Element('li', {
520                 id: hash,
521                 html: html
522             });
523             window.qBittorrent.Filters.tagsFilterContextMenu.addTarget(el);
524             return el;
525         };
527         const torrentsCount = torrentsTable.getRowIds().length;
528         let untagged = 0;
529         for (const key in torrentsTable.rows) {
530             if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]['full_data'].tags.length === 0))
531                 untagged += 1;
532         }
533         tagFilterList.appendChild(createLink(TAGS_ALL, 'QBT_TR(All)QBT_TR[CONTEXT=TagFilterModel]', torrentsCount));
534         tagFilterList.appendChild(createLink(TAGS_UNTAGGED, 'QBT_TR(Untagged)QBT_TR[CONTEXT=TagFilterModel]', untagged));
536         const sortedTags = [];
537         tagList.forEach((tag, hash) => sortedTags.push({
538             tagName: tag.name,
539             tagHash: hash,
540             tagSize: tag.torrents.size
541         }));
542         sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName));
544         for (const { tagName, tagHash, tagSize } of sortedTags)
545             tagFilterList.appendChild(createLink(tagHash, tagName, tagSize));
547         highlightSelectedTag();
548     };
550     const highlightSelectedTag = function() {
551         const tagFilterList = $('tagFilterList');
552         if (!tagFilterList)
553             return;
555         const children = tagFilterList.childNodes;
556         for (let i = 0; i < children.length; ++i)
557             children[i].className = (Number(children[i].id) === selectedTag) ? "selectedFilter" : "";
558     };
560     // getHost emulate the GUI version `QString getHost(const QString &url)`
561     const getHost = function(url) {
562         // We want the hostname.
563         // If failed to parse the domain, original input should be returned
565         if (!/^(?:https?|udp):/i.test(url)) {
566             return url;
567         }
569         try {
570             // hack: URL can not get hostname from udp protocol
571             const parsedUrl = new URL(url.replace(/^udp:/i, 'https:'));
572             // host: "example.com:8443"
573             // hostname: "example.com"
574             const host = parsedUrl.hostname;
575             if (!host) {
576                 return url;
577             }
579             return host;
580         }
581         catch (error) {
582             return url;
583         }
584     };
586     const updateTrackerList = function() {
587         const trackerFilterList = $('trackerFilterList');
588         if (trackerFilterList === null)
589             return;
591         trackerFilterList.getChildren().each(c => c.destroy());
593         const createLink = function(hash, text, count) {
594             const html = '<a href="#" onclick="setTrackerFilter(' + hash + ');return false;">'
595                 + '<img src="images/trackers.svg"/>'
596                 + window.qBittorrent.Misc.escapeHtml(text.replace("%1", count)) + '</a>';
597             const el = new Element('li', {
598                 id: hash,
599                 html: html
600             });
601             window.qBittorrent.Filters.trackersFilterContextMenu.addTarget(el);
602             return el;
603         };
605         const torrentsCount = torrentsTable.getRowIds().length;
606         trackerFilterList.appendChild(createLink(TRACKERS_ALL, 'QBT_TR(All (%1))QBT_TR[CONTEXT=TrackerFiltersList]', torrentsCount));
607         let trackerlessTorrentsCount = 0;
608         for (const key in torrentsTable.rows) {
609             if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]['full_data'].trackers_count === 0))
610                 trackerlessTorrentsCount += 1;
611         }
612         trackerFilterList.appendChild(createLink(TRACKERS_TRACKERLESS, 'QBT_TR(Trackerless (%1))QBT_TR[CONTEXT=TrackerFiltersList]', trackerlessTorrentsCount));
614         // Sort trackers by hostname
615         const sortedList = [];
616         trackerList.forEach((tracker, hash) => sortedList.push({
617             trackerHost: getHost(tracker.url),
618             trackerHash: hash,
619             trackerCount: tracker.torrents.length
620         }));
621         sortedList.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.trackerHost, right.trackerHost));
622         for (const { trackerHost, trackerHash, trackerCount } of sortedList)
623             trackerFilterList.appendChild(createLink(trackerHash, (trackerHost + ' (%1)'), trackerCount));
625         highlightSelectedTracker();
626     };
628     const highlightSelectedTracker = function() {
629         const trackerFilterList = $('trackerFilterList');
630         if (!trackerFilterList)
631             return;
633         const children = trackerFilterList.childNodes;
634         for (const child of children)
635             child.className = (child.id === selectedTracker) ? "selectedFilter" : "";
636     };
638     const setupCopyEventHandler = (function() {
639         let clipboardEvent;
641         return () => {
642             if (clipboardEvent)
643                 clipboardEvent.destroy();
645             clipboardEvent = new ClipboardJS('.copyToClipboard', {
646                 text: function(trigger) {
647                     switch (trigger.id) {
648                         case "copyName":
649                             return copyNameFN();
650                         case "copyInfohash1":
651                             return copyInfohashFN(1);
652                         case "copyInfohash2":
653                             return copyInfohashFN(2);
654                         case "copyMagnetLink":
655                             return copyMagnetLinkFN();
656                         case "copyID":
657                             return copyIdFN();
658                         case "copyComment":
659                             return copyCommentFN();
660                         default:
661                             return "";
662                     }
663                 }
664             });
665         };
666     })();
668     let syncMainDataTimeoutID;
669     let syncRequestInProgress = false;
670     const syncMainData = function() {
671         const url = new URI('api/v2/sync/maindata');
672         url.setData('rid', syncMainDataLastResponseId);
673         const request = new Request.JSON({
674             url: url,
675             noCache: true,
676             method: 'get',
677             onFailure: function() {
678                 const errorDiv = $('error_div');
679                 if (errorDiv)
680                     errorDiv.set('html', 'QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]');
681                 syncRequestInProgress = false;
682                 syncData(2000);
683             },
684             onSuccess: function(response) {
685                 $('error_div').set('html', '');
686                 if (response) {
687                     clearTimeout(torrentsFilterInputTimer);
688                     let torrentsTableSelectedRows;
689                     let update_categories = false;
690                     let updateTags = false;
691                     let updateTrackers = false;
692                     const full_update = (response['full_update'] === true);
693                     if (full_update) {
694                         torrentsTableSelectedRows = torrentsTable.selectedRowsIds();
695                         torrentsTable.clear();
696                         category_list.clear();
697                         tagList.clear();
698                     }
699                     if (response['rid']) {
700                         syncMainDataLastResponseId = response['rid'];
701                     }
702                     if (response['categories']) {
703                         for (const key in response['categories']) {
704                             if (!Object.hasOwn(response['categories'], key))
705                                 continue;
707                             const responseCategory = response['categories'][key];
708                             const categoryHash = window.qBittorrent.Client.genHash(key);
709                             const category = category_list.get(categoryHash);
710                             if (category !== undefined) {
711                                 // only the save path can change for existing categories
712                                 category.savePath = responseCategory.savePath;
713                             }
714                             else {
715                                 category_list.set(categoryHash, {
716                                     name: responseCategory.name,
717                                     savePath: responseCategory.savePath,
718                                     torrents: new Set()
719                                 });
720                             }
721                         }
722                         update_categories = true;
723                     }
724                     if (response['categories_removed']) {
725                         response['categories_removed'].each(function(category) {
726                             const categoryHash = window.qBittorrent.Client.genHash(category);
727                             category_list.delete(categoryHash);
728                         });
729                         update_categories = true;
730                     }
731                     if (response['tags']) {
732                         for (const tag of response['tags']) {
733                             const tagHash = window.qBittorrent.Client.genHash(tag);
734                             if (!tagList.has(tagHash)) {
735                                 tagList.set(tagHash, {
736                                     name: tag,
737                                     torrents: new Set()
738                                 });
739                             }
740                         }
741                         updateTags = true;
742                     }
743                     if (response['tags_removed']) {
744                         for (let i = 0; i < response['tags_removed'].length; ++i) {
745                             const tagHash = window.qBittorrent.Client.genHash(response['tags_removed'][i]);
746                             tagList.delete(tagHash);
747                         }
748                         updateTags = true;
749                     }
750                     if (response['trackers']) {
751                         for (const tracker in response['trackers']) {
752                             const torrents = response['trackers'][tracker];
753                             const hash = window.qBittorrent.Client.genHash(getHost(tracker));
755                             // the reason why we need the merge here is because the web ui api returned trackers may have different url for the same tracker host.
756                             // for example, some private trackers use diff urls for each torrent from the same tracker host.
757                             // then we got the response of `trackers` from qBittorrent api will like:
758                             // {
759                             //     "trackers": {
760                             //         "https://example.com/announce?passkey=identify_info1": ["hash1"],
761                             //         "https://example.com/announce?passkey=identify_info2": ["hash2"],
762                             //         "https://example.com/announce?passkey=identify_info3": ["hash3"]
763                             //     }
764                             // }
765                             // after getHost(), those torrents all belongs to `example.com`
766                             let merged_torrents = torrents;
767                             if (trackerList.has(hash)) {
768                                 merged_torrents = trackerList.get(hash).torrents.concat(torrents);
769                                 // deduplicate is needed when the webui opens in multi tabs
770                                 merged_torrents = merged_torrents.filter((item, pos) => merged_torrents.indexOf(item) === pos);
771                             }
773                             trackerList.set(hash, {
774                                 url: tracker,
775                                 torrents: merged_torrents
776                             });
777                         }
778                         updateTrackers = true;
779                     }
780                     if (response['trackers_removed']) {
781                         for (let i = 0; i < response['trackers_removed'].length; ++i) {
782                             const tracker = response['trackers_removed'][i];
783                             const hash = window.qBittorrent.Client.genHash(getHost(tracker));
784                             trackerList.delete(hash);
785                         }
786                         updateTrackers = true;
787                     }
788                     if (response['torrents']) {
789                         let updateTorrentList = false;
790                         for (const key in response['torrents']) {
791                             response['torrents'][key]['hash'] = key;
792                             response['torrents'][key]['rowId'] = key;
793                             if (response['torrents'][key]['state'])
794                                 response['torrents'][key]['status'] = response['torrents'][key]['state'];
795                             torrentsTable.updateRowData(response['torrents'][key]);
796                             if (addTorrentToCategoryList(response['torrents'][key]))
797                                 update_categories = true;
798                             if (addTorrentToTagList(response['torrents'][key]))
799                                 updateTags = true;
800                             if (response['torrents'][key]['name'])
801                                 updateTorrentList = true;
802                         }
804                         if (updateTorrentList)
805                             setupCopyEventHandler();
806                     }
807                     if (response['torrents_removed'])
808                         response['torrents_removed'].each(function(hash) {
809                             torrentsTable.removeRow(hash);
810                             removeTorrentFromCategoryList(hash);
811                             update_categories = true; // Always to update All category
812                             removeTorrentFromTagList(hash);
813                             updateTags = true; // Always to update All tag
814                         });
815                     torrentsTable.updateTable(full_update);
816                     torrentsTable.altRow();
817                     if (response['server_state']) {
818                         const tmp = response['server_state'];
819                         for (const k in tmp)
820                             serverState[k] = tmp[k];
821                         processServerState();
822                     }
823                     updateFiltersList();
824                     if (update_categories) {
825                         updateCategoryList();
826                         window.qBittorrent.TransferList.contextMenu.updateCategoriesSubMenu(category_list);
827                     }
828                     if (updateTags) {
829                         updateTagList();
830                         window.qBittorrent.TransferList.contextMenu.updateTagsSubMenu(tagList);
831                     }
832                     if (updateTrackers)
833                         updateTrackerList();
835                     if (full_update)
836                         // re-select previously selected rows
837                         torrentsTable.reselectRows(torrentsTableSelectedRows);
838                 }
839                 syncRequestInProgress = false;
840                 syncData(window.qBittorrent.Client.getSyncMainDataInterval());
841             }
842         });
843         syncRequestInProgress = true;
844         request.send();
845     };
847     updateMainData = function() {
848         torrentsTable.updateTable();
849         syncData(100);
850     };
852     const syncData = function(delay) {
853         if (syncRequestInProgress)
854             return;
856         clearTimeout(syncMainDataTimeoutID);
858         if (window.qBittorrent.Client.isStopped())
859             return;
861         syncMainDataTimeoutID = syncMainData.delay(delay);
862     };
864     const processServerState = function() {
865         let transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true);
866         if (serverState.dl_rate_limit > 0)
867             transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_rate_limit, true) + "]";
868         transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_data, false) + ")";
869         $("DlInfos").set('html', transfer_info);
870         transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true);
871         if (serverState.up_rate_limit > 0)
872             transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.up_rate_limit, true) + "]";
873         transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.up_info_data, false) + ")";
874         $("UpInfos").set('html', transfer_info);
876         const qbtVersion = window.qBittorrent.Cache.qbtVersion.get();
878         if (speedInTitle) {
879             document.title = "QBT_TR([D: %1, U: %2] qBittorrent %3)QBT_TR[CONTEXT=MainWindow]".replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true)).replace("%2", window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true)).replace("%3", qbtVersion);
880             document.title += " QBT_TR(Web UI)QBT_TR[CONTEXT=OptionsDialog]";
881         }
882         else
883             document.title = ("qBittorrent " + qbtVersion + " QBT_TR(Web UI)QBT_TR[CONTEXT=OptionsDialog]");
884         $('freeSpaceOnDisk').set('html', 'QBT_TR(Free space: %1)QBT_TR[CONTEXT=HttpServer]'.replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.free_space_on_disk)));
885         $('DHTNodes').set('html', 'QBT_TR(DHT: %1 nodes)QBT_TR[CONTEXT=StatusBar]'.replace("%1", serverState.dht_nodes));
887         // Statistics dialog
888         if (document.getElementById("statisticsContent")) {
889             $('AlltimeDL').set('html', window.qBittorrent.Misc.friendlyUnit(serverState.alltime_dl, false));
890             $('AlltimeUL').set('html', window.qBittorrent.Misc.friendlyUnit(serverState.alltime_ul, false));
891             $('TotalWastedSession').set('html', window.qBittorrent.Misc.friendlyUnit(serverState.total_wasted_session, false));
892             $('GlobalRatio').set('html', serverState.global_ratio);
893             $('TotalPeerConnections').set('html', serverState.total_peer_connections);
894             $('ReadCacheHits').set('html', serverState.read_cache_hits + "%");
895             $('TotalBuffersSize').set('html', window.qBittorrent.Misc.friendlyUnit(serverState.total_buffers_size, false));
896             $('WriteCacheOverload').set('html', serverState.write_cache_overload + "%");
897             $('ReadCacheOverload').set('html', serverState.read_cache_overload + "%");
898             $('QueuedIOJobs').set('html', serverState.queued_io_jobs);
899             $('AverageTimeInQueue').set('html', serverState.average_time_queue + " ms");
900             $('TotalQueuedSize').set('html', window.qBittorrent.Misc.friendlyUnit(serverState.total_queued_size, false));
901         }
903         switch (serverState.connection_status) {
904             case 'connected':
905                 $('connectionStatus').src = 'images/connected.svg';
906                 $('connectionStatus').alt = 'QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]';
907                 $('connectionStatus').title = 'QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]';
908                 break;
909             case 'firewalled':
910                 $('connectionStatus').src = 'images/firewalled.svg';
911                 $('connectionStatus').alt = 'QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]';
912                 $('connectionStatus').title = 'QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]';
913                 break;
914             default:
915                 $('connectionStatus').src = 'images/disconnected.svg';
916                 $('connectionStatus').alt = 'QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]';
917                 $('connectionStatus').title = 'QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]';
918                 break;
919         }
921         if (queueing_enabled != serverState.queueing) {
922             queueing_enabled = serverState.queueing;
923             torrentsTable.columns['priority'].force_hide = !queueing_enabled;
924             torrentsTable.updateColumn('priority');
925             if (queueing_enabled) {
926                 $('topQueuePosItem').removeClass('invisible');
927                 $('increaseQueuePosItem').removeClass('invisible');
928                 $('decreaseQueuePosItem').removeClass('invisible');
929                 $('bottomQueuePosItem').removeClass('invisible');
930                 $('queueingButtons').removeClass('invisible');
931                 $('queueingMenuItems').removeClass('invisible');
932             }
933             else {
934                 $('topQueuePosItem').addClass('invisible');
935                 $('increaseQueuePosItem').addClass('invisible');
936                 $('decreaseQueuePosItem').addClass('invisible');
937                 $('bottomQueuePosItem').addClass('invisible');
938                 $('queueingButtons').addClass('invisible');
939                 $('queueingMenuItems').addClass('invisible');
940             }
941         }
943         if (alternativeSpeedLimits != serverState.use_alt_speed_limits) {
944             alternativeSpeedLimits = serverState.use_alt_speed_limits;
945             updateAltSpeedIcon(alternativeSpeedLimits);
946         }
948         if (useSubcategories != serverState.use_subcategories) {
949             useSubcategories = serverState.use_subcategories;
950             updateCategoryList();
951         }
953         serverSyncMainDataInterval = Math.max(serverState.refresh_interval, 500);
954     };
956     const updateAltSpeedIcon = function(enabled) {
957         if (enabled) {
958             $('alternativeSpeedLimits').src = 'images/slow.svg';
959             $('alternativeSpeedLimits').alt = 'QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]';
960             $('alternativeSpeedLimits').title = 'QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]';
961         }
962         else {
963             $('alternativeSpeedLimits').src = 'images/slow_off.svg';
964             $('alternativeSpeedLimits').alt = 'QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]';
965             $('alternativeSpeedLimits').title = 'QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]';
966         }
967     };
969     $('alternativeSpeedLimits').addEvent('click', function() {
970         // Change icon immediately to give some feedback
971         updateAltSpeedIcon(!alternativeSpeedLimits);
973         new Request({
974             url: 'api/v2/transfer/toggleSpeedLimitsMode',
975             method: 'post',
976             onComplete: function() {
977                 alternativeSpeedLimits = !alternativeSpeedLimits;
978                 updateMainData();
979             },
980             onFailure: function() {
981                 // Restore icon in case of failure
982                 updateAltSpeedIcon(alternativeSpeedLimits);
983             }
984         }).send();
985     });
987     $('DlInfos').addEvent('click', globalDownloadLimitFN);
988     $('UpInfos').addEvent('click', globalUploadLimitFN);
990     $('showTopToolbarLink').addEvent('click', function(e) {
991         showTopToolbar = !showTopToolbar;
992         LocalPreferences.set('show_top_toolbar', showTopToolbar.toString());
993         if (showTopToolbar) {
994             $('showTopToolbarLink').firstChild.style.opacity = '1';
995             $('mochaToolbar').removeClass('invisible');
996         }
997         else {
998             $('showTopToolbarLink').firstChild.style.opacity = '0';
999             $('mochaToolbar').addClass('invisible');
1000         }
1001         MochaUI.Desktop.setDesktopSize();
1002     });
1004     $('showStatusBarLink').addEvent('click', function(e) {
1005         showStatusBar = !showStatusBar;
1006         LocalPreferences.set('show_status_bar', showStatusBar.toString());
1007         if (showStatusBar) {
1008             $('showStatusBarLink').firstChild.style.opacity = '1';
1009             $('desktopFooterWrapper').removeClass('invisible');
1010         }
1011         else {
1012             $('showStatusBarLink').firstChild.style.opacity = '0';
1013             $('desktopFooterWrapper').addClass('invisible');
1014         }
1015         MochaUI.Desktop.setDesktopSize();
1016     });
1018     const registerMagnetHandler = function() {
1019         if (typeof navigator.registerProtocolHandler !== 'function') {
1020             if (window.location.protocol !== 'https:')
1021                 alert("QBT_TR(To use this feature, the WebUI needs to be accessed over HTTPS)QBT_TR[CONTEXT=MainWindow]");
1022             else
1023                 alert("QBT_TR(Your browser does not support this feature)QBT_TR[CONTEXT=MainWindow]");
1024             return;
1025         }
1027         const hashString = location.hash ? location.hash.replace(/^#/, '') : '';
1028         const hashParams = new URLSearchParams(hashString);
1029         hashParams.set('download', '');
1031         const templateHashString = hashParams.toString().replace('download=', 'download=%s');
1032         const templateUrl = location.origin + location.pathname
1033             + location.search + '#' + templateHashString;
1035         navigator.registerProtocolHandler('magnet', templateUrl,
1036             'qBittorrent WebUI magnet handler');
1037     };
1038     $('registerMagnetHandlerLink').addEvent('click', function(e) {
1039         registerMagnetHandler();
1040     });
1042     $('showFiltersSidebarLink').addEvent('click', function(e) {
1043         showFiltersSidebar = !showFiltersSidebar;
1044         LocalPreferences.set('show_filters_sidebar', showFiltersSidebar.toString());
1045         if (showFiltersSidebar) {
1046             $('showFiltersSidebarLink').firstChild.style.opacity = '1';
1047             $('filtersColumn').removeClass('invisible');
1048             $('filtersColumn_handle').removeClass('invisible');
1049         }
1050         else {
1051             $('showFiltersSidebarLink').firstChild.style.opacity = '0';
1052             $('filtersColumn').addClass('invisible');
1053             $('filtersColumn_handle').addClass('invisible');
1054         }
1055         MochaUI.Desktop.setDesktopSize();
1056     });
1058     $('speedInBrowserTitleBarLink').addEvent('click', function(e) {
1059         speedInTitle = !speedInTitle;
1060         LocalPreferences.set('speed_in_browser_title_bar', speedInTitle.toString());
1061         if (speedInTitle)
1062             $('speedInBrowserTitleBarLink').firstChild.style.opacity = '1';
1063         else
1064             $('speedInBrowserTitleBarLink').firstChild.style.opacity = '0';
1065         processServerState();
1066     });
1068     $('showSearchEngineLink').addEvent('click', function(e) {
1069         showSearchEngine = !showSearchEngine;
1070         LocalPreferences.set('show_search_engine', showSearchEngine.toString());
1071         updateTabDisplay();
1072     });
1074     $('showRssReaderLink').addEvent('click', function(e) {
1075         showRssReader = !showRssReader;
1076         LocalPreferences.set('show_rss_reader', showRssReader.toString());
1077         updateTabDisplay();
1078     });
1080     $('showLogViewerLink').addEvent('click', function(e) {
1081         showLogViewer = !showLogViewer;
1082         LocalPreferences.set('show_log_viewer', showLogViewer.toString());
1083         updateTabDisplay();
1084     });
1086     const updateTabDisplay = function() {
1087         if (showRssReader) {
1088             $('showRssReaderLink').firstChild.style.opacity = '1';
1089             $('mainWindowTabs').removeClass('invisible');
1090             $('rssTabLink').removeClass('invisible');
1091             if (!MochaUI.Panels.instances.RssPanel)
1092                 addRssPanel();
1093         }
1094         else {
1095             $('showRssReaderLink').firstChild.style.opacity = '0';
1096             $('rssTabLink').addClass('invisible');
1097             if ($('rssTabLink').hasClass('selected'))
1098                 $("transfersTabLink").click();
1099         }
1101         if (showSearchEngine) {
1102             $('showSearchEngineLink').firstChild.style.opacity = '1';
1103             $('mainWindowTabs').removeClass('invisible');
1104             $('searchTabLink').removeClass('invisible');
1105             if (!MochaUI.Panels.instances.SearchPanel)
1106                 addSearchPanel();
1107         }
1108         else {
1109             $('showSearchEngineLink').firstChild.style.opacity = '0';
1110             $('searchTabLink').addClass('invisible');
1111             if ($('searchTabLink').hasClass('selected'))
1112                 $("transfersTabLink").click();
1113         }
1115         if (showLogViewer) {
1116             $('showLogViewerLink').firstChild.style.opacity = '1';
1117             $('mainWindowTabs').removeClass('invisible');
1118             $('logTabLink').removeClass('invisible');
1119             if (!MochaUI.Panels.instances.LogPanel)
1120                 addLogPanel();
1121         }
1122         else {
1123             $('showLogViewerLink').firstChild.style.opacity = '0';
1124             $('logTabLink').addClass('invisible');
1125             if ($('logTabLink').hasClass('selected'))
1126                 $("transfersTabLink").click();
1127         }
1129         // display no tabs
1130         if (!showRssReader && !showSearchEngine && !showLogViewer)
1131             $('mainWindowTabs').addClass('invisible');
1132     };
1134     $('StatisticsLink').addEvent('click', StatisticsLinkFN);
1136     // main window tabs
1138     const showTransfersTab = function() {
1139         $("filtersColumn").removeClass("invisible");
1140         $("filtersColumn_handle").removeClass("invisible");
1141         $("mainColumn").removeClass("invisible");
1142         $('torrentsFilterToolbar').removeClass("invisible");
1144         customSyncMainDataInterval = null;
1145         syncData(100);
1147         hideSearchTab();
1148         hideRssTab();
1149         hideLogTab();
1150     };
1152     const hideTransfersTab = function() {
1153         $("filtersColumn").addClass("invisible");
1154         $("filtersColumn_handle").addClass("invisible");
1155         $("mainColumn").addClass("invisible");
1156         $('torrentsFilterToolbar').addClass("invisible");
1157         MochaUI.Desktop.resizePanels();
1158     };
1160     const showSearchTab = (function() {
1161         let searchTabInitialized = false;
1163         return () => {
1164             if (!searchTabInitialized) {
1165                 window.qBittorrent.Search.init();
1166                 searchTabInitialized = true;
1167             }
1169             $("searchTabColumn").removeClass("invisible");
1170             customSyncMainDataInterval = 30000;
1171             hideTransfersTab();
1172             hideRssTab();
1173             hideLogTab();
1174         };
1175     })();
1177     const hideSearchTab = function() {
1178         $("searchTabColumn").addClass("invisible");
1179         MochaUI.Desktop.resizePanels();
1180     };
1182     const showRssTab = (function() {
1183         let rssTabInitialized = false;
1185         return () => {
1186             if (!rssTabInitialized) {
1187                 window.qBittorrent.Rss.init();
1188                 rssTabInitialized = true;
1189             }
1190             else {
1191                 window.qBittorrent.Rss.load();
1192             }
1194             $("rssTabColumn").removeClass("invisible");
1195             customSyncMainDataInterval = 30000;
1196             hideTransfersTab();
1197             hideSearchTab();
1198             hideLogTab();
1199         };
1200     })();
1202     const hideRssTab = function() {
1203         $("rssTabColumn").addClass("invisible");
1204         window.qBittorrent.Rss && window.qBittorrent.Rss.unload();
1205         MochaUI.Desktop.resizePanels();
1206     };
1208     const showLogTab = (function() {
1209         let logTabInitialized = false;
1211         return () => {
1212             if (!logTabInitialized) {
1213                 window.qBittorrent.Log.init();
1214                 logTabInitialized = true;
1215             }
1216             else {
1217                 window.qBittorrent.Log.load();
1218             }
1220             $('logTabColumn').removeClass('invisible');
1221             customSyncMainDataInterval = 30000;
1222             hideTransfersTab();
1223             hideSearchTab();
1224             hideRssTab();
1225         };
1226     })();
1228     const hideLogTab = function() {
1229         $('logTabColumn').addClass('invisible');
1230         MochaUI.Desktop.resizePanels();
1231         window.qBittorrent.Log && window.qBittorrent.Log.unload();
1232     };
1234     const addSearchPanel = function() {
1235         new MochaUI.Panel({
1236             id: 'SearchPanel',
1237             title: 'Search',
1238             header: false,
1239             padding: {
1240                 top: 0,
1241                 right: 0,
1242                 bottom: 0,
1243                 left: 0
1244             },
1245             loadMethod: 'xhr',
1246             contentURL: 'views/search.html',
1247             content: '',
1248             column: 'searchTabColumn',
1249             height: null
1250         });
1251     };
1253     const addRssPanel = function() {
1254         new MochaUI.Panel({
1255             id: 'RssPanel',
1256             title: 'Rss',
1257             header: false,
1258             padding: {
1259                 top: 0,
1260                 right: 0,
1261                 bottom: 0,
1262                 left: 0
1263             },
1264             loadMethod: 'xhr',
1265             contentURL: 'views/rss.html',
1266             content: '',
1267             column: 'rssTabColumn',
1268             height: null
1269         });
1270     };
1272     var addLogPanel = function() {
1273         new MochaUI.Panel({
1274             id: 'LogPanel',
1275             title: 'Log',
1276             header: true,
1277             padding: {
1278                 top: 0,
1279                 right: 0,
1280                 bottom: 0,
1281                 left: 0
1282             },
1283             loadMethod: 'xhr',
1284             contentURL: 'views/log.html',
1285             require: {
1286                 css: ['css/vanillaSelectBox.css'],
1287                 js: ['scripts/lib/vanillaSelectBox.js'],
1288             },
1289             tabsURL: 'views/logTabs.html',
1290             tabsOnload: function() {
1291                 MochaUI.initializeTabs('panelTabs');
1293                 $('logMessageLink').addEvent('click', function(e) {
1294                     window.qBittorrent.Log.setCurrentTab('main');
1295                 });
1297                 $('logPeerLink').addEvent('click', function(e) {
1298                     window.qBittorrent.Log.setCurrentTab('peer');
1299                 });
1300             },
1301             collapsible: false,
1302             content: '',
1303             column: 'logTabColumn',
1304             height: null
1305         });
1306     };
1308     const handleDownloadParam = function() {
1309         // Extract torrent URL from download param in WebUI URL hash
1310         const downloadHash = "#download=";
1311         if (location.hash.indexOf(downloadHash) !== 0)
1312             return;
1314         const url = decodeURIComponent(location.hash.substring(downloadHash.length));
1315         // Remove the processed hash from the URL
1316         history.replaceState('', document.title, (location.pathname + location.search));
1317         showDownloadPage([url]);
1318     };
1320     new MochaUI.Panel({
1321         id: 'transferList',
1322         title: 'Panel',
1323         header: false,
1324         padding: {
1325             top: 0,
1326             right: 0,
1327             bottom: 0,
1328             left: 0
1329         },
1330         loadMethod: 'xhr',
1331         contentURL: 'views/transferlist.html',
1332         onContentLoaded: function() {
1333             handleDownloadParam();
1334             updateMainData();
1335         },
1336         column: 'mainColumn',
1337         onResize: saveColumnSizes,
1338         height: null
1339     });
1340     let prop_h = LocalPreferences.get('properties_height_rel');
1341     if ($defined(prop_h))
1342         prop_h = prop_h.toFloat() * Window.getSize().y;
1343     else
1344         prop_h = Window.getSize().y / 2.0;
1345     new MochaUI.Panel({
1346         id: 'propertiesPanel',
1347         title: 'Panel',
1348         header: true,
1349         padding: {
1350             top: 0,
1351             right: 0,
1352             bottom: 0,
1353             left: 0
1354         },
1355         contentURL: 'views/properties.html',
1356         require: {
1357             css: ['css/Tabs.css', 'css/dynamicTable.css'],
1358             js: ['scripts/prop-general.js', 'scripts/prop-trackers.js', 'scripts/prop-peers.js', 'scripts/prop-webseeds.js', 'scripts/prop-files.js'],
1359         },
1360         tabsURL: 'views/propertiesToolbar.html',
1361         tabsOnload: function() {
1362             MochaUI.initializeTabs('propertiesTabs');
1364             updatePropertiesPanel = function() {
1365                 if (!$('prop_general').hasClass('invisible')) {
1366                     if (window.qBittorrent.PropGeneral !== undefined)
1367                         window.qBittorrent.PropGeneral.updateData();
1368                 }
1369                 else if (!$('prop_trackers').hasClass('invisible')) {
1370                     if (window.qBittorrent.PropTrackers !== undefined)
1371                         window.qBittorrent.PropTrackers.updateData();
1372                 }
1373                 else if (!$('prop_peers').hasClass('invisible')) {
1374                     if (window.qBittorrent.PropPeers !== undefined)
1375                         window.qBittorrent.PropPeers.updateData();
1376                 }
1377                 else if (!$('prop_webseeds').hasClass('invisible')) {
1378                     if (window.qBittorrent.PropWebseeds !== undefined)
1379                         window.qBittorrent.PropWebseeds.updateData();
1380                 }
1381                 else if (!$('prop_files').hasClass('invisible')) {
1382                     if (window.qBittorrent.PropFiles !== undefined)
1383                         window.qBittorrent.PropFiles.updateData();
1384                 }
1385             };
1387             $('PropGeneralLink').addEvent('click', function(e) {
1388                 $$('.propertiesTabContent').addClass('invisible');
1389                 $('prop_general').removeClass("invisible");
1390                 hideFilesFilter();
1391                 updatePropertiesPanel();
1392                 LocalPreferences.set('selected_tab', this.id);
1393             });
1395             $('PropTrackersLink').addEvent('click', function(e) {
1396                 $$('.propertiesTabContent').addClass('invisible');
1397                 $('prop_trackers').removeClass("invisible");
1398                 hideFilesFilter();
1399                 updatePropertiesPanel();
1400                 LocalPreferences.set('selected_tab', this.id);
1401             });
1403             $('PropPeersLink').addEvent('click', function(e) {
1404                 $$('.propertiesTabContent').addClass('invisible');
1405                 $('prop_peers').removeClass("invisible");
1406                 hideFilesFilter();
1407                 updatePropertiesPanel();
1408                 LocalPreferences.set('selected_tab', this.id);
1409             });
1411             $('PropWebSeedsLink').addEvent('click', function(e) {
1412                 $$('.propertiesTabContent').addClass('invisible');
1413                 $('prop_webseeds').removeClass("invisible");
1414                 hideFilesFilter();
1415                 updatePropertiesPanel();
1416                 LocalPreferences.set('selected_tab', this.id);
1417             });
1419             $('PropFilesLink').addEvent('click', function(e) {
1420                 $$('.propertiesTabContent').addClass('invisible');
1421                 $('prop_files').removeClass("invisible");
1422                 showFilesFilter();
1423                 updatePropertiesPanel();
1424                 LocalPreferences.set('selected_tab', this.id);
1425             });
1427             $('propertiesPanel_collapseToggle').addEvent('click', function(e) {
1428                 updatePropertiesPanel();
1429             });
1430         },
1431         column: 'mainColumn',
1432         height: prop_h
1433     });
1435     const showFilesFilter = function() {
1436         $('torrentFilesFilterToolbar').removeClass("invisible");
1437     };
1439     const hideFilesFilter = function() {
1440         $('torrentFilesFilterToolbar').addClass("invisible");
1441     };
1443     let prevTorrentsFilterValue;
1444     let torrentsFilterInputTimer = null;
1445     // listen for changes to torrentsFilterInput
1446     $('torrentsFilterInput').addEvent('input', function() {
1447         const value = $('torrentsFilterInput').get("value");
1448         if (value !== prevTorrentsFilterValue) {
1449             prevTorrentsFilterValue = value;
1450             clearTimeout(torrentsFilterInputTimer);
1451             torrentsFilterInputTimer = setTimeout(function() {
1452                 torrentsTable.updateTable(false);
1453             }, 400);
1454         }
1455     });
1457     $('transfersTabLink').addEvent('click', showTransfersTab);
1458     $('searchTabLink').addEvent('click', showSearchTab);
1459     $('rssTabLink').addEvent('click', showRssTab);
1460     $('logTabLink').addEvent('click', showLogTab);
1461     updateTabDisplay();
1463     const registerDragAndDrop = () => {
1464         $('desktop').addEventListener('dragover', (ev) => {
1465             if (ev.preventDefault)
1466                 ev.preventDefault();
1467         });
1469         $('desktop').addEventListener('dragenter', (ev) => {
1470             if (ev.preventDefault)
1471                 ev.preventDefault();
1472         });
1474         $('desktop').addEventListener("drop", (ev) => {
1475             if (ev.preventDefault)
1476                 ev.preventDefault();
1478             const droppedFiles = ev.dataTransfer.files;
1480             if (droppedFiles.length > 0) {
1481                 // dropped files or folders
1483                 // can't handle folder due to cannot put the filelist (from dropped folder)
1484                 // to <input> `files` field
1485                 for (const item of ev.dataTransfer.items) {
1486                     if (item.webkitGetAsEntry().isDirectory)
1487                         return;
1488                 }
1490                 const id = 'uploadPage';
1491                 new MochaUI.Window({
1492                     id: id,
1493                     title: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
1494                     loadMethod: 'iframe',
1495                     contentURL: new URI("upload.html").toString(),
1496                     addClass: 'windowFrame', // fixes iframe scrolling on iOS Safari
1497                     scrollbars: true,
1498                     maximizable: false,
1499                     paddingVertical: 0,
1500                     paddingHorizontal: 0,
1501                     width: loadWindowWidth(id, 500),
1502                     height: loadWindowHeight(id, 460),
1503                     onResize: () => {
1504                         saveWindowSize(id);
1505                     },
1506                     onContentLoaded: () => {
1507                         const fileInput = $(`${id}_iframe`).contentDocument.getElementById('fileselect');
1508                         fileInput.files = droppedFiles;
1509                     }
1510                 });
1511             }
1513             const droppedText = ev.dataTransfer.getData("text");
1514             if (droppedText.length > 0) {
1515                 // dropped text
1517                 const urls = droppedText.split('\n')
1518                     .map((str) => str.trim())
1519                     .filter((str) => {
1520                         const lowercaseStr = str.toLowerCase();
1521                         return lowercaseStr.startsWith("http:")
1522                             || lowercaseStr.startsWith("https:")
1523                             || lowercaseStr.startsWith("magnet:")
1524                             || ((str.length === 40) && !(/[^0-9A-Fa-f]/.test(str))) // v1 hex-encoded SHA-1 info-hash
1525                             || ((str.length === 32) && !(/[^2-7A-Za-z]/.test(str))); // v1 Base32 encoded SHA-1 info-hash
1526                     });
1528                 if (urls.length <= 0)
1529                     return;
1531                 const id = 'downloadPage';
1532                 const contentURI = new URI('download.html').setData("urls", urls.map(encodeURIComponent).join("|"));
1533                 new MochaUI.Window({
1534                     id: id,
1535                     title: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
1536                     loadMethod: 'iframe',
1537                     contentURL: contentURI.toString(),
1538                     addClass: 'windowFrame', // fixes iframe scrolling on iOS Safari
1539                     scrollbars: true,
1540                     maximizable: false,
1541                     closable: true,
1542                     paddingVertical: 0,
1543                     paddingHorizontal: 0,
1544                     width: loadWindowWidth(id, 500),
1545                     height: loadWindowHeight(id, 600),
1546                     onResize: () => {
1547                         saveWindowSize(id);
1548                     }
1549                 });
1550             }
1551         });
1552     };
1553     registerDragAndDrop();
1555     new Keyboard({
1556         defaultEventType: 'keydown',
1557         events: {
1558             'ctrl+a': function(event) {
1559                 if (event.target.nodeName == "INPUT" || event.target.nodeName == "TEXTAREA")
1560                     return;
1561                 if (event.target.isContentEditable)
1562                     return;
1563                 torrentsTable.selectAll();
1564                 event.preventDefault();
1565             },
1566             'delete': function(event) {
1567                 if (event.target.nodeName == "INPUT" || event.target.nodeName == "TEXTAREA")
1568                     return;
1569                 if (event.target.isContentEditable)
1570                     return;
1571                 deleteFN();
1572                 event.preventDefault();
1573             },
1574             'shift+delete': (event) => {
1575                 if (event.target.nodeName == "INPUT" || event.target.nodeName == "TEXTAREA")
1576                     return;
1577                 if (event.target.isContentEditable)
1578                     return;
1579                 deleteFN(true);
1580                 event.preventDefault();
1581             }
1582         }
1583     }).activate();
1586 window.addEventListener("load", () => {
1587     // fetch various data and store it in memory
1588     window.qBittorrent.Cache.buildInfo.init();
1589     window.qBittorrent.Cache.preferences.init();
1590     window.qBittorrent.Cache.qbtVersion.init();