WebUI: Improve accuracy of trackers list
[qBittorrent.git] / src / webui / www / private / scripts / client.js
blobde62e43b57ac0c6341b003c1a6c9e6cce4d4f7eb
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>
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:
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
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.
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
44 const closeWindows = function() {
45 MochaUI.closeAll();
48 const genHash = function(string) {
49 // origins:
50 // https://stackoverflow.com/a/8831937
51 // https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0
52 let hash = 0;
53 for (let i = 0; i < string.length; ++i)
54 hash = ((Math.imul(hash, 31) + string.charCodeAt(i)) | 0);
55 return hash;
58 const getSyncMainDataInterval = function() {
59 return customSyncMainDataInterval ? customSyncMainDataInterval : serverSyncMainDataInterval;
62 let stopped = false;
63 const isStopped = () => {
64 return stopped;
67 const stop = () => {
68 stopped = true;
71 const mainTitle = () => {
72 const emDash = '\u2014';
73 const qbtVersion = window.qBittorrent.Cache.qbtVersion.get();
74 const suffix = window.qBittorrent.Cache.preferences.get()['app_instance_name'] || '';
75 const title = `qBittorrent ${qbtVersion} QBT_TR(WebUI)QBT_TR[CONTEXT=OptionsDialog]`
76 + ((suffix.length > 0) ? ` ${emDash} ${suffix}` : '');
77 return title;
80 return exports();
81 })();
82 Object.freeze(window.qBittorrent.Client);
84 // TODO: move global functions/variables into some namespace/scope
86 this.torrentsTable = new window.qBittorrent.DynamicTable.TorrentsTable();
88 let updatePropertiesPanel = function() {};
90 this.updateMainData = function() {};
91 let alternativeSpeedLimits = false;
92 let queueing_enabled = true;
93 let serverSyncMainDataInterval = 1500;
94 let customSyncMainDataInterval = null;
95 let useSubcategories = true;
97 /* Categories filter */
98 const CATEGORIES_ALL = 1;
99 const CATEGORIES_UNCATEGORIZED = 2;
101 const category_list = new Map();
103 let selected_category = Number(LocalPreferences.get('selected_category', CATEGORIES_ALL));
104 let setCategoryFilter = function() {};
106 /* Tags filter */
107 const TAGS_ALL = 1;
108 const TAGS_UNTAGGED = 2;
110 const tagList = new Map();
112 let selectedTag = Number(LocalPreferences.get('selected_tag', TAGS_ALL));
113 let setTagFilter = function() {};
115 /* Trackers filter */
116 const TRACKERS_ALL = 1;
117 const TRACKERS_TRACKERLESS = 2;
119 /** @type Map<number, {host: string, trackerTorrentMap: Map<string, string[]>}> **/
120 const trackerList = new Map();
122 let selectedTracker = LocalPreferences.get('selected_tracker', TRACKERS_ALL);
123 let setTrackerFilter = function() {};
125 /* All filters */
126 let selected_filter = LocalPreferences.get('selected_filter', 'all');
127 let setFilter = function() {};
128 let toggleFilterDisplay = function() {};
130 window.addEventListener("DOMContentLoaded", function() {
131 const saveColumnSizes = function() {
132 const filters_width = $('Filters').getSize().x;
133 const properties_height_rel = $('propertiesPanel').getSize().y / Window.getSize().y;
134 LocalPreferences.set('filters_width', filters_width);
135 LocalPreferences.set('properties_height_rel', properties_height_rel);
138 window.addEvent('resize', function() {
139 // only save sizes if the columns are visible
140 if (!$("mainColumn").hasClass("invisible"))
141 saveColumnSizes.delay(200); // Resizing might takes some time.
144 /*MochaUI.Desktop = new MochaUI.Desktop();
145 MochaUI.Desktop.desktop.setStyles({
146 'background': '#fff',
147 'visibility': 'visible'
148 });*/
149 MochaUI.Desktop.initialize();
151 const buildTransfersTab = function() {
152 let filt_w = LocalPreferences.get('filters_width');
153 if ($defined(filt_w))
154 filt_w = filt_w.toInt();
155 else
156 filt_w = 120;
157 new MochaUI.Column({
158 id: 'filtersColumn',
159 placement: 'left',
160 onResize: saveColumnSizes,
161 width: filt_w,
162 resizeLimit: [1, 300]
165 new MochaUI.Column({
166 id: 'mainColumn',
167 placement: 'main'
171 const buildSearchTab = function() {
172 new MochaUI.Column({
173 id: 'searchTabColumn',
174 placement: 'main',
175 width: null
178 // start off hidden
179 $("searchTabColumn").addClass("invisible");
182 const buildRssTab = function() {
183 new MochaUI.Column({
184 id: 'rssTabColumn',
185 placement: 'main',
186 width: null
189 // start off hidden
190 $("rssTabColumn").addClass("invisible");
193 const buildLogTab = function() {
194 new MochaUI.Column({
195 id: 'logTabColumn',
196 placement: 'main',
197 width: null
200 // start off hidden
201 $('logTabColumn').addClass('invisible');
204 buildTransfersTab();
205 buildSearchTab();
206 buildRssTab();
207 buildLogTab();
208 MochaUI.initializeTabs('mainWindowTabsList');
210 setCategoryFilter = function(hash) {
211 selected_category = hash;
212 LocalPreferences.set('selected_category', selected_category);
213 highlightSelectedCategory();
214 if (typeof torrentsTable.tableBody != 'undefined')
215 updateMainData();
218 setTagFilter = function(hash) {
219 selectedTag = hash;
220 LocalPreferences.set('selected_tag', selectedTag);
221 highlightSelectedTag();
222 if (torrentsTable.tableBody !== undefined)
223 updateMainData();
226 setTrackerFilter = function(hash) {
227 selectedTracker = hash.toString();
228 LocalPreferences.set('selected_tracker', selectedTracker);
229 highlightSelectedTracker();
230 if (torrentsTable.tableBody !== undefined)
231 updateMainData();
234 setFilter = function(f) {
235 // Visually Select the right filter
236 $("all_filter").removeClass("selectedFilter");
237 $("downloading_filter").removeClass("selectedFilter");
238 $("seeding_filter").removeClass("selectedFilter");
239 $("completed_filter").removeClass("selectedFilter");
240 $("stopped_filter").removeClass("selectedFilter");
241 $("running_filter").removeClass("selectedFilter");
242 $("active_filter").removeClass("selectedFilter");
243 $("inactive_filter").removeClass("selectedFilter");
244 $("stalled_filter").removeClass("selectedFilter");
245 $("stalled_uploading_filter").removeClass("selectedFilter");
246 $("stalled_downloading_filter").removeClass("selectedFilter");
247 $("checking_filter").removeClass("selectedFilter");
248 $("moving_filter").removeClass("selectedFilter");
249 $("errored_filter").removeClass("selectedFilter");
250 $(f + "_filter").addClass("selectedFilter");
251 selected_filter = f;
252 LocalPreferences.set('selected_filter', f);
253 // Reload torrents
254 if (typeof torrentsTable.tableBody != 'undefined')
255 updateMainData();
258 toggleFilterDisplay = function(filter) {
259 const element = filter + "FilterList";
260 LocalPreferences.set('filter_' + filter + "_collapsed", !$(element).hasClass("invisible"));
261 $(element).toggleClass("invisible");
262 const parent = $(element).getParent(".filterWrapper");
263 const toggleIcon = $(parent).getChildren(".filterTitle img");
264 if (toggleIcon)
265 toggleIcon[0].toggleClass("rotate");
268 new MochaUI.Panel({
269 id: 'Filters',
270 title: 'Panel',
271 header: false,
272 padding: {
273 top: 0,
274 right: 0,
275 bottom: 0,
276 left: 0
278 loadMethod: 'xhr',
279 contentURL: 'views/filters.html',
280 onContentLoaded: function() {
281 setFilter(selected_filter);
283 column: 'filtersColumn',
284 height: 300
286 initializeWindows();
288 // Show Top Toolbar is enabled by default
289 let showTopToolbar = LocalPreferences.get('show_top_toolbar', 'true') === "true";
290 if (!showTopToolbar) {
291 $('showTopToolbarLink').firstChild.style.opacity = '0';
292 $('mochaToolbar').addClass('invisible');
295 // Show Status Bar is enabled by default
296 let showStatusBar = LocalPreferences.get('show_status_bar', 'true') === "true";
297 if (!showStatusBar) {
298 $('showStatusBarLink').firstChild.style.opacity = '0';
299 $('desktopFooterWrapper').addClass('invisible');
302 // Show Filters Sidebar is enabled by default
303 let showFiltersSidebar = LocalPreferences.get('show_filters_sidebar', 'true') === "true";
304 if (!showFiltersSidebar) {
305 $('showFiltersSidebarLink').firstChild.style.opacity = '0';
306 $('filtersColumn').addClass('invisible');
307 $('filtersColumn_handle').addClass('invisible');
310 let speedInTitle = LocalPreferences.get('speed_in_browser_title_bar') === "true";
311 if (!speedInTitle)
312 $('speedInBrowserTitleBarLink').firstChild.style.opacity = '0';
314 // After showing/hiding the toolbar + status bar
315 let showSearchEngine = LocalPreferences.get('show_search_engine') !== "false";
316 let showRssReader = LocalPreferences.get('show_rss_reader') !== "false";
317 let showLogViewer = LocalPreferences.get('show_log_viewer') === 'true';
319 // After Show Top Toolbar
320 MochaUI.Desktop.setDesktopSize();
322 let syncMainDataLastResponseId = 0;
323 const serverState = {};
325 const removeTorrentFromCategoryList = function(hash) {
326 if (!hash)
327 return false;
329 let removed = false;
330 category_list.forEach((category) => {
331 const deleteResult = category.torrents.delete(hash);
332 removed ||= deleteResult;
335 return removed;
338 const addTorrentToCategoryList = function(torrent) {
339 const category = torrent['category'];
340 if (typeof category === 'undefined')
341 return false;
343 const hash = torrent['hash'];
344 if (category.length === 0) { // Empty category
345 removeTorrentFromCategoryList(hash);
346 return true;
349 const categoryHash = window.qBittorrent.Client.genHash(category);
350 if (!category_list.has(categoryHash)) { // This should not happen
351 category_list.set(categoryHash, {
352 name: category,
353 torrents: new Set()
357 const torrents = category_list.get(categoryHash).torrents;
358 if (!torrents.has(hash)) {
359 removeTorrentFromCategoryList(hash);
360 torrents.add(hash);
361 return true;
363 return false;
366 const removeTorrentFromTagList = function(hash) {
367 if (!hash)
368 return false;
370 let removed = false;
371 tagList.forEach((tag) => {
372 const deleteResult = tag.torrents.delete(hash);
373 removed ||= deleteResult;
376 return removed;
379 const addTorrentToTagList = function(torrent) {
380 if (torrent['tags'] === undefined) // Tags haven't changed
381 return false;
383 const hash = torrent['hash'];
384 removeTorrentFromTagList(hash);
386 if (torrent['tags'].length === 0) // No tags
387 return true;
389 const tags = torrent['tags'].split(',');
390 let added = false;
391 for (let i = 0; i < tags.length; ++i) {
392 const tagHash = window.qBittorrent.Client.genHash(tags[i].trim());
393 if (!tagList.has(tagHash)) { // This should not happen
394 tagList.set(tagHash, {
395 name: tags,
396 torrents: new Set()
400 const torrents = tagList.get(tagHash).torrents;
401 if (!torrents.has(hash)) {
402 torrents.add(hash);
403 added = true;
406 return added;
409 const updateFilter = function(filter, filterTitle) {
410 $(filter + '_filter').firstChild.childNodes[1].nodeValue = filterTitle.replace('%1', torrentsTable.getFilteredTorrentsNumber(filter, CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL));
413 const updateFiltersList = function() {
414 updateFilter('all', 'QBT_TR(All (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
415 updateFilter('downloading', 'QBT_TR(Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
416 updateFilter('seeding', 'QBT_TR(Seeding (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
417 updateFilter('completed', 'QBT_TR(Completed (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
418 updateFilter('running', 'QBT_TR(Running (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
419 updateFilter('stopped', 'QBT_TR(Stopped (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
420 updateFilter('active', 'QBT_TR(Active (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
421 updateFilter('inactive', 'QBT_TR(Inactive (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
422 updateFilter('stalled', 'QBT_TR(Stalled (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
423 updateFilter('stalled_uploading', 'QBT_TR(Stalled Uploading (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
424 updateFilter('stalled_downloading', 'QBT_TR(Stalled Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
425 updateFilter('checking', 'QBT_TR(Checking (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
426 updateFilter('moving', 'QBT_TR(Moving (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
427 updateFilter('errored', 'QBT_TR(Errored (%1))QBT_TR[CONTEXT=StatusFilterWidget]');
430 const updateCategoryList = function() {
431 const categoryList = $('categoryFilterList');
432 if (!categoryList)
433 return;
434 categoryList.getChildren().each(c => c.destroy());
436 const create_link = function(hash, text, count) {
437 let display_name = text;
438 let margin_left = 0;
439 if (useSubcategories) {
440 const category_path = text.split("/");
441 display_name = category_path[category_path.length - 1];
442 margin_left = (category_path.length - 1) * 20;
445 const html = `<a href="#" style="margin-left: ${margin_left}px;" onclick="setCategoryFilter(${hash}); return false;">`
446 + '<img src="images/view-categories.svg"/>'
447 + window.qBittorrent.Misc.escapeHtml(display_name) + ' (' + count + ')' + '</a>';
448 const el = new Element('li', {
449 id: hash,
450 html: html
452 window.qBittorrent.Filters.categoriesFilterContextMenu.addTarget(el);
453 return el;
456 const all = torrentsTable.getRowIds().length;
457 let uncategorized = 0;
458 for (const key in torrentsTable.rows) {
459 if (!Object.hasOwn(torrentsTable.rows, key))
460 continue;
462 const row = torrentsTable.rows[key];
463 if (row['full_data'].category.length === 0)
464 uncategorized += 1;
466 categoryList.appendChild(create_link(CATEGORIES_ALL, 'QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]', all));
467 categoryList.appendChild(create_link(CATEGORIES_UNCATEGORIZED, 'QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]', uncategorized));
469 const sortedCategories = [];
470 category_list.forEach((category, hash) => sortedCategories.push({
471 categoryName: category.name,
472 categoryHash: hash,
473 categoryCount: category.torrents.size
474 }));
475 sortedCategories.sort((left, right) => {
476 const leftSegments = left.categoryName.split('/');
477 const rightSegments = right.categoryName.split('/');
479 for (let i = 0, iMax = Math.min(leftSegments.length, rightSegments.length); i < iMax; ++i) {
480 const compareResult = window.qBittorrent.Misc.naturalSortCollator.compare(
481 leftSegments[i], rightSegments[i]);
482 if (compareResult !== 0)
483 return compareResult;
486 return leftSegments.length - rightSegments.length;
489 for (let i = 0; i < sortedCategories.length; ++i) {
490 const { categoryName, categoryHash } = sortedCategories[i];
491 let { categoryCount } = sortedCategories[i];
493 if (useSubcategories) {
494 for (let j = (i + 1);
495 ((j < sortedCategories.length) && sortedCategories[j].categoryName.startsWith(categoryName + "/")); ++j) {
496 categoryCount += sortedCategories[j].categoryCount;
500 categoryList.appendChild(create_link(categoryHash, categoryName, categoryCount));
503 highlightSelectedCategory();
506 const highlightSelectedCategory = function() {
507 const categoryList = $('categoryFilterList');
508 if (!categoryList)
509 return;
510 const children = categoryList.childNodes;
511 for (let i = 0; i < children.length; ++i) {
512 if (Number(children[i].id) === selected_category)
513 children[i].className = "selectedFilter";
514 else
515 children[i].className = "";
519 const updateTagList = function() {
520 const tagFilterList = $('tagFilterList');
521 if (tagFilterList === null)
522 return;
524 tagFilterList.getChildren().each(c => c.destroy());
526 const createLink = function(hash, text, count) {
527 const html = `<a href="#" onclick="setTagFilter(${hash}); return false;">`
528 + '<img src="images/tags.svg"/>'
529 + window.qBittorrent.Misc.escapeHtml(text) + ' (' + count + ')' + '</a>';
530 const el = new Element('li', {
531 id: hash,
532 html: html
534 window.qBittorrent.Filters.tagsFilterContextMenu.addTarget(el);
535 return el;
538 const torrentsCount = torrentsTable.getRowIds().length;
539 let untagged = 0;
540 for (const key in torrentsTable.rows) {
541 if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]['full_data'].tags.length === 0))
542 untagged += 1;
544 tagFilterList.appendChild(createLink(TAGS_ALL, 'QBT_TR(All)QBT_TR[CONTEXT=TagFilterModel]', torrentsCount));
545 tagFilterList.appendChild(createLink(TAGS_UNTAGGED, 'QBT_TR(Untagged)QBT_TR[CONTEXT=TagFilterModel]', untagged));
547 const sortedTags = [];
548 tagList.forEach((tag, hash) => sortedTags.push({
549 tagName: tag.name,
550 tagHash: hash,
551 tagSize: tag.torrents.size
552 }));
553 sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName));
555 for (const { tagName, tagHash, tagSize } of sortedTags)
556 tagFilterList.appendChild(createLink(tagHash, tagName, tagSize));
558 highlightSelectedTag();
561 const highlightSelectedTag = function() {
562 const tagFilterList = $('tagFilterList');
563 if (!tagFilterList)
564 return;
566 const children = tagFilterList.childNodes;
567 for (let i = 0; i < children.length; ++i)
568 children[i].className = (Number(children[i].id) === selectedTag) ? "selectedFilter" : "";
571 // getHost emulate the GUI version `QString getHost(const QString &url)`
572 const getHost = function(url) {
573 // We want the hostname.
574 // If failed to parse the domain, original input should be returned
576 if (!/^(?:https?|udp):/i.test(url)) {
577 return url;
580 try {
581 // hack: URL can not get hostname from udp protocol
582 const parsedUrl = new URL(url.replace(/^udp:/i, 'https:'));
583 // host: "example.com:8443"
584 // hostname: "example.com"
585 const host = parsedUrl.hostname;
586 if (!host) {
587 return url;
590 return host;
592 catch (error) {
593 return url;
597 const updateTrackerList = function() {
598 const trackerFilterList = $('trackerFilterList');
599 if (trackerFilterList === null)
600 return;
602 trackerFilterList.getChildren().each(c => c.destroy());
604 const createLink = function(hash, text, count) {
605 const html = '<a href="#" onclick="setTrackerFilter(' + hash + ');return false;">'
606 + '<img src="images/trackers.svg"/>'
607 + window.qBittorrent.Misc.escapeHtml(text.replace("%1", count)) + '</a>';
608 const el = new Element('li', {
609 id: hash,
610 html: html
612 window.qBittorrent.Filters.trackersFilterContextMenu.addTarget(el);
613 return el;
616 const torrentsCount = torrentsTable.getRowIds().length;
617 trackerFilterList.appendChild(createLink(TRACKERS_ALL, 'QBT_TR(All (%1))QBT_TR[CONTEXT=TrackerFiltersList]', torrentsCount));
618 let trackerlessTorrentsCount = 0;
619 for (const key in torrentsTable.rows) {
620 if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]['full_data'].trackers_count === 0))
621 trackerlessTorrentsCount += 1;
623 trackerFilterList.appendChild(createLink(TRACKERS_TRACKERLESS, 'QBT_TR(Trackerless (%1))QBT_TR[CONTEXT=TrackerFiltersList]', trackerlessTorrentsCount));
625 // Sort trackers by hostname
626 const sortedList = [];
627 trackerList.forEach(({ host, trackerTorrentMap }, hash) => {
628 const uniqueTorrents = new Set();
629 for (const torrents of trackerTorrentMap.values()) {
630 for (const torrent of torrents) {
631 uniqueTorrents.add(torrent);
635 sortedList.push({
636 trackerHost: host,
637 trackerHash: hash,
638 trackerCount: uniqueTorrents.size,
641 sortedList.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.trackerHost, right.trackerHost));
642 for (const { trackerHost, trackerHash, trackerCount } of sortedList)
643 trackerFilterList.appendChild(createLink(trackerHash, (trackerHost + ' (%1)'), trackerCount));
645 highlightSelectedTracker();
648 const highlightSelectedTracker = function() {
649 const trackerFilterList = $('trackerFilterList');
650 if (!trackerFilterList)
651 return;
653 const children = trackerFilterList.childNodes;
654 for (const child of children)
655 child.className = (child.id === selectedTracker) ? "selectedFilter" : "";
658 const setupCopyEventHandler = (function() {
659 let clipboardEvent;
661 return () => {
662 if (clipboardEvent)
663 clipboardEvent.destroy();
665 clipboardEvent = new ClipboardJS('.copyToClipboard', {
666 text: function(trigger) {
667 switch (trigger.id) {
668 case "copyName":
669 return copyNameFN();
670 case "copyInfohash1":
671 return copyInfohashFN(1);
672 case "copyInfohash2":
673 return copyInfohashFN(2);
674 case "copyMagnetLink":
675 return copyMagnetLinkFN();
676 case "copyID":
677 return copyIdFN();
678 case "copyComment":
679 return copyCommentFN();
680 default:
681 return "";
686 })();
688 let syncMainDataTimeoutID;
689 let syncRequestInProgress = false;
690 const syncMainData = function() {
691 const url = new URI('api/v2/sync/maindata');
692 url.setData('rid', syncMainDataLastResponseId);
693 const request = new Request.JSON({
694 url: url,
695 noCache: true,
696 method: 'get',
697 onFailure: function() {
698 const errorDiv = $('error_div');
699 if (errorDiv)
700 errorDiv.set('html', 'QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]');
701 syncRequestInProgress = false;
702 syncData(2000);
704 onSuccess: function(response) {
705 $('error_div').set('html', '');
706 if (response) {
707 clearTimeout(torrentsFilterInputTimer);
708 torrentsFilterInputTimer = -1;
710 let torrentsTableSelectedRows;
711 let update_categories = false;
712 let updateTags = false;
713 let updateTrackers = false;
714 const full_update = (response['full_update'] === true);
715 if (full_update) {
716 torrentsTableSelectedRows = torrentsTable.selectedRowsIds();
717 torrentsTable.clear();
718 category_list.clear();
719 tagList.clear();
721 if (response['rid']) {
722 syncMainDataLastResponseId = response['rid'];
724 if (response['categories']) {
725 for (const key in response['categories']) {
726 if (!Object.hasOwn(response['categories'], key))
727 continue;
729 const responseCategory = response['categories'][key];
730 const categoryHash = window.qBittorrent.Client.genHash(key);
731 const category = category_list.get(categoryHash);
732 if (category !== undefined) {
733 // only the save path can change for existing categories
734 category.savePath = responseCategory.savePath;
736 else {
737 category_list.set(categoryHash, {
738 name: responseCategory.name,
739 savePath: responseCategory.savePath,
740 torrents: new Set()
744 update_categories = true;
746 if (response['categories_removed']) {
747 response['categories_removed'].each(function(category) {
748 const categoryHash = window.qBittorrent.Client.genHash(category);
749 category_list.delete(categoryHash);
751 update_categories = true;
753 if (response['tags']) {
754 for (const tag of response['tags']) {
755 const tagHash = window.qBittorrent.Client.genHash(tag);
756 if (!tagList.has(tagHash)) {
757 tagList.set(tagHash, {
758 name: tag,
759 torrents: new Set()
763 updateTags = true;
765 if (response['tags_removed']) {
766 for (let i = 0; i < response['tags_removed'].length; ++i) {
767 const tagHash = window.qBittorrent.Client.genHash(response['tags_removed'][i]);
768 tagList.delete(tagHash);
770 updateTags = true;
772 if (response['trackers']) {
773 for (const [tracker, torrents] of Object.entries(response['trackers'])) {
774 const host = getHost(tracker);
775 const hash = window.qBittorrent.Client.genHash(host);
777 let trackerListItem = trackerList.get(hash);
778 if (trackerListItem === undefined) {
779 trackerListItem = { host: host, trackerTorrentMap: new Map() };
780 trackerList.set(hash, trackerListItem);
783 trackerListItem.trackerTorrentMap.set(tracker, [...torrents]);
785 updateTrackers = true;
787 if (response['trackers_removed']) {
788 for (let i = 0; i < response['trackers_removed'].length; ++i) {
789 const tracker = response['trackers_removed'][i];
790 const hash = window.qBittorrent.Client.genHash(getHost(tracker));
791 const trackerListEntry = trackerList.get(hash);
792 if (trackerListEntry) {
793 trackerListEntry.trackerTorrentMap.delete(tracker);
796 updateTrackers = true;
798 if (response['torrents']) {
799 let updateTorrentList = false;
800 for (const key in response['torrents']) {
801 response['torrents'][key]['hash'] = key;
802 response['torrents'][key]['rowId'] = key;
803 if (response['torrents'][key]['state'])
804 response['torrents'][key]['status'] = response['torrents'][key]['state'];
805 torrentsTable.updateRowData(response['torrents'][key]);
806 if (addTorrentToCategoryList(response['torrents'][key]))
807 update_categories = true;
808 if (addTorrentToTagList(response['torrents'][key]))
809 updateTags = true;
810 if (response['torrents'][key]['name'])
811 updateTorrentList = true;
814 if (updateTorrentList)
815 setupCopyEventHandler();
817 if (response['torrents_removed'])
818 response['torrents_removed'].each(function(hash) {
819 torrentsTable.removeRow(hash);
820 removeTorrentFromCategoryList(hash);
821 update_categories = true; // Always to update All category
822 removeTorrentFromTagList(hash);
823 updateTags = true; // Always to update All tag
825 torrentsTable.updateTable(full_update);
826 torrentsTable.altRow();
827 if (response['server_state']) {
828 const tmp = response['server_state'];
829 for (const k in tmp)
830 serverState[k] = tmp[k];
831 processServerState();
833 updateFiltersList();
834 if (update_categories) {
835 updateCategoryList();
836 window.qBittorrent.TransferList.contextMenu.updateCategoriesSubMenu(category_list);
838 if (updateTags) {
839 updateTagList();
840 window.qBittorrent.TransferList.contextMenu.updateTagsSubMenu(tagList);
842 if (updateTrackers)
843 updateTrackerList();
845 if (full_update)
846 // re-select previously selected rows
847 torrentsTable.reselectRows(torrentsTableSelectedRows);
849 syncRequestInProgress = false;
850 syncData(window.qBittorrent.Client.getSyncMainDataInterval());
853 syncRequestInProgress = true;
854 request.send();
857 updateMainData = function() {
858 torrentsTable.updateTable();
859 syncData(100);
862 const syncData = function(delay) {
863 if (syncRequestInProgress)
864 return;
866 clearTimeout(syncMainDataTimeoutID);
868 if (window.qBittorrent.Client.isStopped())
869 return;
871 syncMainDataTimeoutID = syncMainData.delay(delay);
874 const processServerState = function() {
875 let transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true);
876 if (serverState.dl_rate_limit > 0)
877 transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_rate_limit, true) + "]";
878 transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_data, false) + ")";
879 $("DlInfos").set('html', transfer_info);
880 transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true);
881 if (serverState.up_rate_limit > 0)
882 transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.up_rate_limit, true) + "]";
883 transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.up_info_data, false) + ")";
884 $("UpInfos").set('html', transfer_info);
886 document.title = (speedInTitle
887 ? (`QBT_TR([D: %1, U: %2])QBT_TR[CONTEXT=MainWindow] `
888 .replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true))
889 .replace("%2", window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true)))
890 : '')
891 + window.qBittorrent.Client.mainTitle();
893 $('freeSpaceOnDisk').set('html', 'QBT_TR(Free space: %1)QBT_TR[CONTEXT=HttpServer]'.replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.free_space_on_disk)));
894 $('DHTNodes').set('html', 'QBT_TR(DHT: %1 nodes)QBT_TR[CONTEXT=StatusBar]'.replace("%1", serverState.dht_nodes));
896 // Statistics dialog
897 if (document.getElementById("statisticsContent")) {
898 $('AlltimeDL').set('html', window.qBittorrent.Misc.friendlyUnit(serverState.alltime_dl, false));
899 $('AlltimeUL').set('html', window.qBittorrent.Misc.friendlyUnit(serverState.alltime_ul, false));
900 $('TotalWastedSession').set('html', window.qBittorrent.Misc.friendlyUnit(serverState.total_wasted_session, false));
901 $('GlobalRatio').set('html', serverState.global_ratio);
902 $('TotalPeerConnections').set('html', serverState.total_peer_connections);
903 $('ReadCacheHits').set('html', serverState.read_cache_hits + "%");
904 $('TotalBuffersSize').set('html', window.qBittorrent.Misc.friendlyUnit(serverState.total_buffers_size, false));
905 $('WriteCacheOverload').set('html', serverState.write_cache_overload + "%");
906 $('ReadCacheOverload').set('html', serverState.read_cache_overload + "%");
907 $('QueuedIOJobs').set('html', serverState.queued_io_jobs);
908 $('AverageTimeInQueue').set('html', serverState.average_time_queue + " ms");
909 $('TotalQueuedSize').set('html', window.qBittorrent.Misc.friendlyUnit(serverState.total_queued_size, false));
912 switch (serverState.connection_status) {
913 case 'connected':
914 $('connectionStatus').src = 'images/connected.svg';
915 $('connectionStatus').alt = 'QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]';
916 $('connectionStatus').title = 'QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]';
917 break;
918 case 'firewalled':
919 $('connectionStatus').src = 'images/firewalled.svg';
920 $('connectionStatus').alt = 'QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]';
921 $('connectionStatus').title = 'QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]';
922 break;
923 default:
924 $('connectionStatus').src = 'images/disconnected.svg';
925 $('connectionStatus').alt = 'QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]';
926 $('connectionStatus').title = 'QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]';
927 break;
930 if (queueing_enabled != serverState.queueing) {
931 queueing_enabled = serverState.queueing;
932 torrentsTable.columns['priority'].force_hide = !queueing_enabled;
933 torrentsTable.updateColumn('priority');
934 if (queueing_enabled) {
935 $('topQueuePosItem').removeClass('invisible');
936 $('increaseQueuePosItem').removeClass('invisible');
937 $('decreaseQueuePosItem').removeClass('invisible');
938 $('bottomQueuePosItem').removeClass('invisible');
939 $('queueingButtons').removeClass('invisible');
940 $('queueingMenuItems').removeClass('invisible');
942 else {
943 $('topQueuePosItem').addClass('invisible');
944 $('increaseQueuePosItem').addClass('invisible');
945 $('decreaseQueuePosItem').addClass('invisible');
946 $('bottomQueuePosItem').addClass('invisible');
947 $('queueingButtons').addClass('invisible');
948 $('queueingMenuItems').addClass('invisible');
952 if (alternativeSpeedLimits != serverState.use_alt_speed_limits) {
953 alternativeSpeedLimits = serverState.use_alt_speed_limits;
954 updateAltSpeedIcon(alternativeSpeedLimits);
957 if (useSubcategories != serverState.use_subcategories) {
958 useSubcategories = serverState.use_subcategories;
959 updateCategoryList();
962 serverSyncMainDataInterval = Math.max(serverState.refresh_interval, 500);
965 const updateAltSpeedIcon = function(enabled) {
966 if (enabled) {
967 $('alternativeSpeedLimits').src = 'images/slow.svg';
968 $('alternativeSpeedLimits').alt = 'QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]';
969 $('alternativeSpeedLimits').title = 'QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]';
971 else {
972 $('alternativeSpeedLimits').src = 'images/slow_off.svg';
973 $('alternativeSpeedLimits').alt = 'QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]';
974 $('alternativeSpeedLimits').title = 'QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]';
978 $('alternativeSpeedLimits').addEvent('click', function() {
979 // Change icon immediately to give some feedback
980 updateAltSpeedIcon(!alternativeSpeedLimits);
982 new Request({
983 url: 'api/v2/transfer/toggleSpeedLimitsMode',
984 method: 'post',
985 onComplete: function() {
986 alternativeSpeedLimits = !alternativeSpeedLimits;
987 updateMainData();
989 onFailure: function() {
990 // Restore icon in case of failure
991 updateAltSpeedIcon(alternativeSpeedLimits);
993 }).send();
996 $('DlInfos').addEvent('click', globalDownloadLimitFN);
997 $('UpInfos').addEvent('click', globalUploadLimitFN);
999 $('showTopToolbarLink').addEvent('click', function(e) {
1000 showTopToolbar = !showTopToolbar;
1001 LocalPreferences.set('show_top_toolbar', showTopToolbar.toString());
1002 if (showTopToolbar) {
1003 $('showTopToolbarLink').firstChild.style.opacity = '1';
1004 $('mochaToolbar').removeClass('invisible');
1006 else {
1007 $('showTopToolbarLink').firstChild.style.opacity = '0';
1008 $('mochaToolbar').addClass('invisible');
1010 MochaUI.Desktop.setDesktopSize();
1013 $('showStatusBarLink').addEvent('click', function(e) {
1014 showStatusBar = !showStatusBar;
1015 LocalPreferences.set('show_status_bar', showStatusBar.toString());
1016 if (showStatusBar) {
1017 $('showStatusBarLink').firstChild.style.opacity = '1';
1018 $('desktopFooterWrapper').removeClass('invisible');
1020 else {
1021 $('showStatusBarLink').firstChild.style.opacity = '0';
1022 $('desktopFooterWrapper').addClass('invisible');
1024 MochaUI.Desktop.setDesktopSize();
1027 const registerMagnetHandler = function() {
1028 if (typeof navigator.registerProtocolHandler !== 'function') {
1029 if (window.location.protocol !== 'https:')
1030 alert("QBT_TR(To use this feature, the WebUI needs to be accessed over HTTPS)QBT_TR[CONTEXT=MainWindow]");
1031 else
1032 alert("QBT_TR(Your browser does not support this feature)QBT_TR[CONTEXT=MainWindow]");
1033 return;
1036 const hashString = location.hash ? location.hash.replace(/^#/, '') : '';
1037 const hashParams = new URLSearchParams(hashString);
1038 hashParams.set('download', '');
1040 const templateHashString = hashParams.toString().replace('download=', 'download=%s');
1041 const templateUrl = location.origin + location.pathname
1042 + location.search + '#' + templateHashString;
1044 navigator.registerProtocolHandler('magnet', templateUrl,
1045 'qBittorrent WebUI magnet handler');
1047 $('registerMagnetHandlerLink').addEvent('click', function(e) {
1048 registerMagnetHandler();
1051 $('showFiltersSidebarLink').addEvent('click', function(e) {
1052 showFiltersSidebar = !showFiltersSidebar;
1053 LocalPreferences.set('show_filters_sidebar', showFiltersSidebar.toString());
1054 if (showFiltersSidebar) {
1055 $('showFiltersSidebarLink').firstChild.style.opacity = '1';
1056 $('filtersColumn').removeClass('invisible');
1057 $('filtersColumn_handle').removeClass('invisible');
1059 else {
1060 $('showFiltersSidebarLink').firstChild.style.opacity = '0';
1061 $('filtersColumn').addClass('invisible');
1062 $('filtersColumn_handle').addClass('invisible');
1064 MochaUI.Desktop.setDesktopSize();
1067 $('speedInBrowserTitleBarLink').addEvent('click', function(e) {
1068 speedInTitle = !speedInTitle;
1069 LocalPreferences.set('speed_in_browser_title_bar', speedInTitle.toString());
1070 if (speedInTitle)
1071 $('speedInBrowserTitleBarLink').firstChild.style.opacity = '1';
1072 else
1073 $('speedInBrowserTitleBarLink').firstChild.style.opacity = '0';
1074 processServerState();
1077 $('showSearchEngineLink').addEvent('click', function(e) {
1078 showSearchEngine = !showSearchEngine;
1079 LocalPreferences.set('show_search_engine', showSearchEngine.toString());
1080 updateTabDisplay();
1083 $('showRssReaderLink').addEvent('click', function(e) {
1084 showRssReader = !showRssReader;
1085 LocalPreferences.set('show_rss_reader', showRssReader.toString());
1086 updateTabDisplay();
1089 $('showLogViewerLink').addEvent('click', function(e) {
1090 showLogViewer = !showLogViewer;
1091 LocalPreferences.set('show_log_viewer', showLogViewer.toString());
1092 updateTabDisplay();
1095 const updateTabDisplay = function() {
1096 if (showRssReader) {
1097 $('showRssReaderLink').firstChild.style.opacity = '1';
1098 $('mainWindowTabs').removeClass('invisible');
1099 $('rssTabLink').removeClass('invisible');
1100 if (!MochaUI.Panels.instances.RssPanel)
1101 addRssPanel();
1103 else {
1104 $('showRssReaderLink').firstChild.style.opacity = '0';
1105 $('rssTabLink').addClass('invisible');
1106 if ($('rssTabLink').hasClass('selected'))
1107 $("transfersTabLink").click();
1110 if (showSearchEngine) {
1111 $('showSearchEngineLink').firstChild.style.opacity = '1';
1112 $('mainWindowTabs').removeClass('invisible');
1113 $('searchTabLink').removeClass('invisible');
1114 if (!MochaUI.Panels.instances.SearchPanel)
1115 addSearchPanel();
1117 else {
1118 $('showSearchEngineLink').firstChild.style.opacity = '0';
1119 $('searchTabLink').addClass('invisible');
1120 if ($('searchTabLink').hasClass('selected'))
1121 $("transfersTabLink").click();
1124 if (showLogViewer) {
1125 $('showLogViewerLink').firstChild.style.opacity = '1';
1126 $('mainWindowTabs').removeClass('invisible');
1127 $('logTabLink').removeClass('invisible');
1128 if (!MochaUI.Panels.instances.LogPanel)
1129 addLogPanel();
1131 else {
1132 $('showLogViewerLink').firstChild.style.opacity = '0';
1133 $('logTabLink').addClass('invisible');
1134 if ($('logTabLink').hasClass('selected'))
1135 $("transfersTabLink").click();
1138 // display no tabs
1139 if (!showRssReader && !showSearchEngine && !showLogViewer)
1140 $('mainWindowTabs').addClass('invisible');
1143 $('StatisticsLink').addEvent('click', StatisticsLinkFN);
1145 // main window tabs
1147 const showTransfersTab = function() {
1148 const showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
1149 if (showFiltersSidebar) {
1150 $("filtersColumn").removeClass("invisible");
1151 $("filtersColumn_handle").removeClass("invisible");
1153 $("mainColumn").removeClass("invisible");
1154 $('torrentsFilterToolbar').removeClass("invisible");
1156 customSyncMainDataInterval = null;
1157 syncData(100);
1159 hideSearchTab();
1160 hideRssTab();
1161 hideLogTab();
1164 const hideTransfersTab = function() {
1165 $("filtersColumn").addClass("invisible");
1166 $("filtersColumn_handle").addClass("invisible");
1167 $("mainColumn").addClass("invisible");
1168 $('torrentsFilterToolbar').addClass("invisible");
1169 MochaUI.Desktop.resizePanels();
1172 const showSearchTab = (function() {
1173 let searchTabInitialized = false;
1175 return () => {
1176 if (!searchTabInitialized) {
1177 window.qBittorrent.Search.init();
1178 searchTabInitialized = true;
1181 $("searchTabColumn").removeClass("invisible");
1182 customSyncMainDataInterval = 30000;
1183 hideTransfersTab();
1184 hideRssTab();
1185 hideLogTab();
1187 })();
1189 const hideSearchTab = function() {
1190 $("searchTabColumn").addClass("invisible");
1191 MochaUI.Desktop.resizePanels();
1194 const showRssTab = (function() {
1195 let rssTabInitialized = false;
1197 return () => {
1198 if (!rssTabInitialized) {
1199 window.qBittorrent.Rss.init();
1200 rssTabInitialized = true;
1202 else {
1203 window.qBittorrent.Rss.load();
1206 $("rssTabColumn").removeClass("invisible");
1207 customSyncMainDataInterval = 30000;
1208 hideTransfersTab();
1209 hideSearchTab();
1210 hideLogTab();
1212 })();
1214 const hideRssTab = function() {
1215 $("rssTabColumn").addClass("invisible");
1216 window.qBittorrent.Rss && window.qBittorrent.Rss.unload();
1217 MochaUI.Desktop.resizePanels();
1220 const showLogTab = (function() {
1221 let logTabInitialized = false;
1223 return () => {
1224 if (!logTabInitialized) {
1225 window.qBittorrent.Log.init();
1226 logTabInitialized = true;
1228 else {
1229 window.qBittorrent.Log.load();
1232 $('logTabColumn').removeClass('invisible');
1233 customSyncMainDataInterval = 30000;
1234 hideTransfersTab();
1235 hideSearchTab();
1236 hideRssTab();
1238 })();
1240 const hideLogTab = function() {
1241 $('logTabColumn').addClass('invisible');
1242 MochaUI.Desktop.resizePanels();
1243 window.qBittorrent.Log && window.qBittorrent.Log.unload();
1246 const addSearchPanel = function() {
1247 new MochaUI.Panel({
1248 id: 'SearchPanel',
1249 title: 'Search',
1250 header: false,
1251 padding: {
1252 top: 0,
1253 right: 0,
1254 bottom: 0,
1255 left: 0
1257 loadMethod: 'xhr',
1258 contentURL: 'views/search.html',
1259 content: '',
1260 column: 'searchTabColumn',
1261 height: null
1265 const addRssPanel = function() {
1266 new MochaUI.Panel({
1267 id: 'RssPanel',
1268 title: 'Rss',
1269 header: false,
1270 padding: {
1271 top: 0,
1272 right: 0,
1273 bottom: 0,
1274 left: 0
1276 loadMethod: 'xhr',
1277 contentURL: 'views/rss.html',
1278 content: '',
1279 column: 'rssTabColumn',
1280 height: null
1284 var addLogPanel = function() {
1285 new MochaUI.Panel({
1286 id: 'LogPanel',
1287 title: 'Log',
1288 header: true,
1289 padding: {
1290 top: 0,
1291 right: 0,
1292 bottom: 0,
1293 left: 0
1295 loadMethod: 'xhr',
1296 contentURL: 'views/log.html',
1297 require: {
1298 css: ['css/vanillaSelectBox.css'],
1299 js: ['scripts/lib/vanillaSelectBox.js'],
1301 tabsURL: 'views/logTabs.html',
1302 tabsOnload: function() {
1303 MochaUI.initializeTabs('panelTabs');
1305 $('logMessageLink').addEvent('click', function(e) {
1306 window.qBittorrent.Log.setCurrentTab('main');
1309 $('logPeerLink').addEvent('click', function(e) {
1310 window.qBittorrent.Log.setCurrentTab('peer');
1313 collapsible: false,
1314 content: '',
1315 column: 'logTabColumn',
1316 height: null
1320 const handleDownloadParam = function() {
1321 // Extract torrent URL from download param in WebUI URL hash
1322 const downloadHash = "#download=";
1323 if (location.hash.indexOf(downloadHash) !== 0)
1324 return;
1326 const url = decodeURIComponent(location.hash.substring(downloadHash.length));
1327 // Remove the processed hash from the URL
1328 history.replaceState('', document.title, (location.pathname + location.search));
1329 showDownloadPage([url]);
1332 new MochaUI.Panel({
1333 id: 'transferList',
1334 title: 'Panel',
1335 header: false,
1336 padding: {
1337 top: 0,
1338 right: 0,
1339 bottom: 0,
1340 left: 0
1342 loadMethod: 'xhr',
1343 contentURL: 'views/transferlist.html',
1344 onContentLoaded: function() {
1345 handleDownloadParam();
1346 updateMainData();
1348 column: 'mainColumn',
1349 onResize: saveColumnSizes,
1350 height: null
1352 let prop_h = LocalPreferences.get('properties_height_rel');
1353 if ($defined(prop_h))
1354 prop_h = prop_h.toFloat() * Window.getSize().y;
1355 else
1356 prop_h = Window.getSize().y / 2.0;
1357 new MochaUI.Panel({
1358 id: 'propertiesPanel',
1359 title: 'Panel',
1360 header: true,
1361 padding: {
1362 top: 0,
1363 right: 0,
1364 bottom: 0,
1365 left: 0
1367 contentURL: 'views/properties.html',
1368 require: {
1369 css: ['css/Tabs.css', 'css/dynamicTable.css'],
1370 js: ['scripts/prop-general.js', 'scripts/prop-trackers.js', 'scripts/prop-peers.js', 'scripts/prop-webseeds.js', 'scripts/prop-files.js'],
1372 tabsURL: 'views/propertiesToolbar.html',
1373 tabsOnload: function() {
1374 MochaUI.initializeTabs('propertiesTabs');
1376 updatePropertiesPanel = function() {
1377 if (!$('prop_general').hasClass('invisible')) {
1378 if (window.qBittorrent.PropGeneral !== undefined)
1379 window.qBittorrent.PropGeneral.updateData();
1381 else if (!$('prop_trackers').hasClass('invisible')) {
1382 if (window.qBittorrent.PropTrackers !== undefined)
1383 window.qBittorrent.PropTrackers.updateData();
1385 else if (!$('prop_peers').hasClass('invisible')) {
1386 if (window.qBittorrent.PropPeers !== undefined)
1387 window.qBittorrent.PropPeers.updateData();
1389 else if (!$('prop_webseeds').hasClass('invisible')) {
1390 if (window.qBittorrent.PropWebseeds !== undefined)
1391 window.qBittorrent.PropWebseeds.updateData();
1393 else if (!$('prop_files').hasClass('invisible')) {
1394 if (window.qBittorrent.PropFiles !== undefined)
1395 window.qBittorrent.PropFiles.updateData();
1399 $('PropGeneralLink').addEvent('click', function(e) {
1400 $$('.propertiesTabContent').addClass('invisible');
1401 $('prop_general').removeClass("invisible");
1402 hideFilesFilter();
1403 updatePropertiesPanel();
1404 LocalPreferences.set('selected_tab', this.id);
1407 $('PropTrackersLink').addEvent('click', function(e) {
1408 $$('.propertiesTabContent').addClass('invisible');
1409 $('prop_trackers').removeClass("invisible");
1410 hideFilesFilter();
1411 updatePropertiesPanel();
1412 LocalPreferences.set('selected_tab', this.id);
1415 $('PropPeersLink').addEvent('click', function(e) {
1416 $$('.propertiesTabContent').addClass('invisible');
1417 $('prop_peers').removeClass("invisible");
1418 hideFilesFilter();
1419 updatePropertiesPanel();
1420 LocalPreferences.set('selected_tab', this.id);
1423 $('PropWebSeedsLink').addEvent('click', function(e) {
1424 $$('.propertiesTabContent').addClass('invisible');
1425 $('prop_webseeds').removeClass("invisible");
1426 hideFilesFilter();
1427 updatePropertiesPanel();
1428 LocalPreferences.set('selected_tab', this.id);
1431 $('PropFilesLink').addEvent('click', function(e) {
1432 $$('.propertiesTabContent').addClass('invisible');
1433 $('prop_files').removeClass("invisible");
1434 showFilesFilter();
1435 updatePropertiesPanel();
1436 LocalPreferences.set('selected_tab', this.id);
1439 $('propertiesPanel_collapseToggle').addEvent('click', function(e) {
1440 updatePropertiesPanel();
1443 column: 'mainColumn',
1444 height: prop_h
1447 const showFilesFilter = function() {
1448 $('torrentFilesFilterToolbar').removeClass("invisible");
1451 const hideFilesFilter = function() {
1452 $('torrentFilesFilterToolbar').addClass("invisible");
1455 // listen for changes to torrentsFilterInput
1456 let torrentsFilterInputTimer = -1;
1457 $('torrentsFilterInput').addEvent('input', () => {
1458 clearTimeout(torrentsFilterInputTimer);
1459 torrentsFilterInputTimer = setTimeout(() => {
1460 torrentsFilterInputTimer = -1;
1461 torrentsTable.updateTable();
1462 }, window.qBittorrent.Misc.FILTER_INPUT_DELAY);
1464 $('torrentsFilterRegexBox').addEvent('change', () => {
1465 torrentsTable.updateTable();
1468 $('transfersTabLink').addEvent('click', showTransfersTab);
1469 $('searchTabLink').addEvent('click', showSearchTab);
1470 $('rssTabLink').addEvent('click', showRssTab);
1471 $('logTabLink').addEvent('click', showLogTab);
1472 updateTabDisplay();
1474 const registerDragAndDrop = () => {
1475 $('desktop').addEventListener('dragover', (ev) => {
1476 if (ev.preventDefault)
1477 ev.preventDefault();
1480 $('desktop').addEventListener('dragenter', (ev) => {
1481 if (ev.preventDefault)
1482 ev.preventDefault();
1485 $('desktop').addEventListener("drop", (ev) => {
1486 if (ev.preventDefault)
1487 ev.preventDefault();
1489 const droppedFiles = ev.dataTransfer.files;
1491 if (droppedFiles.length > 0) {
1492 // dropped files or folders
1494 // can't handle folder due to cannot put the filelist (from dropped folder)
1495 // to <input> `files` field
1496 for (const item of ev.dataTransfer.items) {
1497 if (item.webkitGetAsEntry().isDirectory)
1498 return;
1501 const id = 'uploadPage';
1502 new MochaUI.Window({
1503 id: id,
1504 title: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
1505 loadMethod: 'iframe',
1506 contentURL: new URI("upload.html").toString(),
1507 addClass: 'windowFrame', // fixes iframe scrolling on iOS Safari
1508 scrollbars: true,
1509 maximizable: false,
1510 paddingVertical: 0,
1511 paddingHorizontal: 0,
1512 width: loadWindowWidth(id, 500),
1513 height: loadWindowHeight(id, 460),
1514 onResize: () => {
1515 saveWindowSize(id);
1517 onContentLoaded: () => {
1518 const fileInput = $(`${id}_iframe`).contentDocument.getElementById('fileselect');
1519 fileInput.files = droppedFiles;
1524 const droppedText = ev.dataTransfer.getData("text");
1525 if (droppedText.length > 0) {
1526 // dropped text
1528 const urls = droppedText.split('\n')
1529 .map((str) => str.trim())
1530 .filter((str) => {
1531 const lowercaseStr = str.toLowerCase();
1532 return lowercaseStr.startsWith("http:")
1533 || lowercaseStr.startsWith("https:")
1534 || lowercaseStr.startsWith("magnet:")
1535 || ((str.length === 40) && !(/[^0-9A-Fa-f]/.test(str))) // v1 hex-encoded SHA-1 info-hash
1536 || ((str.length === 32) && !(/[^2-7A-Za-z]/.test(str))); // v1 Base32 encoded SHA-1 info-hash
1539 if (urls.length <= 0)
1540 return;
1542 const id = 'downloadPage';
1543 const contentURI = new URI('download.html').setData("urls", urls.map(encodeURIComponent).join("|"));
1544 new MochaUI.Window({
1545 id: id,
1546 title: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
1547 loadMethod: 'iframe',
1548 contentURL: contentURI.toString(),
1549 addClass: 'windowFrame', // fixes iframe scrolling on iOS Safari
1550 scrollbars: true,
1551 maximizable: false,
1552 closable: true,
1553 paddingVertical: 0,
1554 paddingHorizontal: 0,
1555 width: loadWindowWidth(id, 500),
1556 height: loadWindowHeight(id, 600),
1557 onResize: () => {
1558 saveWindowSize(id);
1564 registerDragAndDrop();
1566 new Keyboard({
1567 defaultEventType: 'keydown',
1568 events: {
1569 'ctrl+a': function(event) {
1570 if (event.target.nodeName == "INPUT" || event.target.nodeName == "TEXTAREA")
1571 return;
1572 if (event.target.isContentEditable)
1573 return;
1574 torrentsTable.selectAll();
1575 event.preventDefault();
1577 'delete': function(event) {
1578 if (event.target.nodeName == "INPUT" || event.target.nodeName == "TEXTAREA")
1579 return;
1580 if (event.target.isContentEditable)
1581 return;
1582 deleteFN();
1583 event.preventDefault();
1585 'shift+delete': (event) => {
1586 if (event.target.nodeName == "INPUT" || event.target.nodeName == "TEXTAREA")
1587 return;
1588 if (event.target.isContentEditable)
1589 return;
1590 deleteFN(true);
1591 event.preventDefault();
1594 }).activate();
1597 window.addEventListener("load", () => {
1598 // fetch various data and store it in memory
1599 window.qBittorrent.Cache.buildInfo.init();
1600 window.qBittorrent.Cache.preferences.init();
1601 window.qBittorrent.Cache.qbtVersion.init();