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
28 if (window.qBittorrent === undefined) {
29 window.qBittorrent = {};
32 window.qBittorrent.Client = (() => {
33 const exports = () => {
35 closeWindows: closeWindows,
37 getSyncMainDataInterval: getSyncMainDataInterval,
43 const closeWindows = function() {
47 const genHash = function(string) {
49 // https://stackoverflow.com/a/8831937
50 // https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0
52 for (let i = 0; i < string.length; ++i)
53 hash = ((Math.imul(hash, 31) + string.charCodeAt(i)) | 0);
57 const getSyncMainDataInterval = function() {
58 return customSyncMainDataInterval ? customSyncMainDataInterval : serverSyncMainDataInterval;
62 const isStopped = () => {
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() {};
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() {};
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);
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.
133 /*MochaUI.Desktop = new MochaUI.Desktop();
134 MochaUI.Desktop.desktop.setStyles({
135 'background': '#fff',
136 'visibility': 'visible'
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();
149 onResize: saveColumnSizes,
151 resizeLimit: [1, 300]
160 const buildSearchTab = function() {
162 id: 'searchTabColumn',
168 $("searchTabColumn").addClass("invisible");
171 const buildRssTab = function() {
179 $("rssTabColumn").addClass("invisible");
182 const buildLogTab = function() {
190 $('logTabColumn').addClass('invisible');
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')
207 setTagFilter = function(hash) {
209 LocalPreferences.set('selected_tag', selectedTag);
210 highlightSelectedTag();
211 if (torrentsTable.tableBody !== undefined)
215 setTrackerFilter = function(hash) {
216 selectedTracker = hash.toString();
217 LocalPreferences.set('selected_tracker', selectedTracker);
218 highlightSelectedTracker();
219 if (torrentsTable.tableBody !== undefined)
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");
241 LocalPreferences.set('selected_filter', f);
243 if (typeof torrentsTable.tableBody != 'undefined')
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");
254 toggleIcon[0].toggleClass("rotate");
268 contentURL: 'views/filters.html',
269 onContentLoaded: function() {
270 setFilter(selected_filter);
272 column: 'filtersColumn',
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');
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');
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');
299 let speedInTitle = LocalPreferences.get('speed_in_browser_title_bar') == "true";
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) {
319 category_list.forEach((category) => {
320 const deleteResult = category.torrents.delete(hash);
321 removed ||= deleteResult;
327 const addTorrentToCategoryList = function(torrent) {
328 const category = torrent['category'];
329 if (typeof category === 'undefined')
332 const hash = torrent['hash'];
333 if (category.length === 0) { // Empty category
334 removeTorrentFromCategoryList(hash);
338 const categoryHash = window.qBittorrent.Client.genHash(category);
339 if (!category_list.has(categoryHash)) { // This should not happen
340 category_list.set(categoryHash, {
346 const torrents = category_list.get(categoryHash).torrents;
347 if (!torrents.has(hash)) {
348 removeTorrentFromCategoryList(hash);
355 const removeTorrentFromTagList = function(hash) {
360 tagList.forEach((tag) => {
361 const deleteResult = tag.torrents.delete(hash);
362 removed ||= deleteResult;
368 const addTorrentToTagList = function(torrent) {
369 if (torrent['tags'] === undefined) // Tags haven't changed
372 const hash = torrent['hash'];
373 removeTorrentFromTagList(hash);
375 if (torrent['tags'].length === 0) // No tags
378 const tags = torrent['tags'].split(',');
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, {
389 const torrents = tagList.get(tagHash).torrents;
390 if (!torrents.has(hash)) {
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));
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]');
419 const updateCategoryList = function() {
420 const categoryList = $('categoryFilterList');
423 categoryList.getChildren().each(c => c.destroy());
425 const create_link = function(hash, text, count) {
426 let display_name = text;
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;
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', {
441 window.qBittorrent.Filters.categoriesFilterContextMenu.addTarget(el);
445 const all = torrentsTable.getRowIds().length;
446 let uncategorized = 0;
447 for (const key in torrentsTable.rows) {
448 if (!Object.hasOwn(torrentsTable.rows, key))
451 const row = torrentsTable.rows[key];
452 if (row['full_data'].category.length === 0)
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,
462 categoryCount: category.torrents.size
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;
475 return leftSegments.length - rightSegments.length;
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;
489 categoryList.appendChild(create_link(categoryHash, categoryName, categoryCount));
492 highlightSelectedCategory();
495 const highlightSelectedCategory = function() {
496 const categoryList = $('categoryFilterList');
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";
504 children[i].className = "";
508 const updateTagList = function() {
509 const tagFilterList = $('tagFilterList');
510 if (tagFilterList === null)
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', {
523 window.qBittorrent.Filters.tagsFilterContextMenu.addTarget(el);
527 const torrentsCount = torrentsTable.getRowIds().length;
529 for (const key in torrentsTable.rows) {
530 if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]['full_data'].tags.length === 0))
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({
540 tagSize: tag.torrents.size
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();
550 const highlightSelectedTag = function() {
551 const tagFilterList = $('tagFilterList');
555 const children = tagFilterList.childNodes;
556 for (let i = 0; i < children.length; ++i)
557 children[i].className = (Number(children[i].id) === selectedTag) ? "selectedFilter" : "";
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)) {
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;
586 const updateTrackerList = function() {
587 const trackerFilterList = $('trackerFilterList');
588 if (trackerFilterList === null)
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', {
601 window.qBittorrent.Filters.trackersFilterContextMenu.addTarget(el);
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;
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),
619 trackerCount: tracker.torrents.length
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();
628 const highlightSelectedTracker = function() {
629 const trackerFilterList = $('trackerFilterList');
630 if (!trackerFilterList)
633 const children = trackerFilterList.childNodes;
634 for (const child of children)
635 child.className = (child.id === selectedTracker) ? "selectedFilter" : "";
638 const setupCopyEventHandler = (function() {
643 clipboardEvent.destroy();
645 clipboardEvent = new ClipboardJS('.copyToClipboard', {
646 text: function(trigger) {
647 switch (trigger.id) {
650 case "copyInfohash1":
651 return copyInfohashFN(1);
652 case "copyInfohash2":
653 return copyInfohashFN(2);
654 case "copyMagnetLink":
655 return copyMagnetLinkFN();
659 return copyCommentFN();
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({
677 onFailure: function() {
678 const errorDiv = $('error_div');
680 errorDiv.set('html', 'QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]');
681 syncRequestInProgress = false;
684 onSuccess: function(response) {
685 $('error_div').set('html', '');
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);
694 torrentsTableSelectedRows = torrentsTable.selectedRowsIds();
695 torrentsTable.clear();
696 category_list.clear();
699 if (response['rid']) {
700 syncMainDataLastResponseId = response['rid'];
702 if (response['categories']) {
703 for (const key in response['categories']) {
704 if (!Object.hasOwn(response['categories'], key))
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;
715 category_list.set(categoryHash, {
716 name: responseCategory.name,
717 savePath: responseCategory.savePath,
722 update_categories = true;
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);
729 update_categories = true;
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, {
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);
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:
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"]
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);
773 trackerList.set(hash, {
775 torrents: merged_torrents
778 updateTrackers = true;
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);
786 updateTrackers = true;
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]))
800 if (response['torrents'][key]['name'])
801 updateTorrentList = true;
804 if (updateTorrentList)
805 setupCopyEventHandler();
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
815 torrentsTable.updateTable(full_update);
816 torrentsTable.altRow();
817 if (response['server_state']) {
818 const tmp = response['server_state'];
820 serverState[k] = tmp[k];
821 processServerState();
824 if (update_categories) {
825 updateCategoryList();
826 window.qBittorrent.TransferList.contextMenu.updateCategoriesSubMenu(category_list);
830 window.qBittorrent.TransferList.contextMenu.updateTagsSubMenu(tagList);
836 // re-select previously selected rows
837 torrentsTable.reselectRows(torrentsTableSelectedRows);
839 syncRequestInProgress = false;
840 syncData(window.qBittorrent.Client.getSyncMainDataInterval());
843 syncRequestInProgress = true;
847 updateMainData = function() {
848 torrentsTable.updateTable();
852 const syncData = function(delay) {
853 if (syncRequestInProgress)
856 clearTimeout(syncMainDataTimeoutID);
858 if (window.qBittorrent.Client.isStopped())
861 syncMainDataTimeoutID = syncMainData.delay(delay);
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();
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]";
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));
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));
903 switch (serverState.connection_status) {
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]';
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]';
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]';
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');
934 $('topQueuePosItem').addClass('invisible');
935 $('increaseQueuePosItem').addClass('invisible');
936 $('decreaseQueuePosItem').addClass('invisible');
937 $('bottomQueuePosItem').addClass('invisible');
938 $('queueingButtons').addClass('invisible');
939 $('queueingMenuItems').addClass('invisible');
943 if (alternativeSpeedLimits != serverState.use_alt_speed_limits) {
944 alternativeSpeedLimits = serverState.use_alt_speed_limits;
945 updateAltSpeedIcon(alternativeSpeedLimits);
948 if (useSubcategories != serverState.use_subcategories) {
949 useSubcategories = serverState.use_subcategories;
950 updateCategoryList();
953 serverSyncMainDataInterval = Math.max(serverState.refresh_interval, 500);
956 const updateAltSpeedIcon = function(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]';
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]';
969 $('alternativeSpeedLimits').addEvent('click', function() {
970 // Change icon immediately to give some feedback
971 updateAltSpeedIcon(!alternativeSpeedLimits);
974 url: 'api/v2/transfer/toggleSpeedLimitsMode',
976 onComplete: function() {
977 alternativeSpeedLimits = !alternativeSpeedLimits;
980 onFailure: function() {
981 // Restore icon in case of failure
982 updateAltSpeedIcon(alternativeSpeedLimits);
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');
998 $('showTopToolbarLink').firstChild.style.opacity = '0';
999 $('mochaToolbar').addClass('invisible');
1001 MochaUI.Desktop.setDesktopSize();
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');
1012 $('showStatusBarLink').firstChild.style.opacity = '0';
1013 $('desktopFooterWrapper').addClass('invisible');
1015 MochaUI.Desktop.setDesktopSize();
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]");
1023 alert("QBT_TR(Your browser does not support this feature)QBT_TR[CONTEXT=MainWindow]");
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');
1038 $('registerMagnetHandlerLink').addEvent('click', function(e) {
1039 registerMagnetHandler();
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');
1051 $('showFiltersSidebarLink').firstChild.style.opacity = '0';
1052 $('filtersColumn').addClass('invisible');
1053 $('filtersColumn_handle').addClass('invisible');
1055 MochaUI.Desktop.setDesktopSize();
1058 $('speedInBrowserTitleBarLink').addEvent('click', function(e) {
1059 speedInTitle = !speedInTitle;
1060 LocalPreferences.set('speed_in_browser_title_bar', speedInTitle.toString());
1062 $('speedInBrowserTitleBarLink').firstChild.style.opacity = '1';
1064 $('speedInBrowserTitleBarLink').firstChild.style.opacity = '0';
1065 processServerState();
1068 $('showSearchEngineLink').addEvent('click', function(e) {
1069 showSearchEngine = !showSearchEngine;
1070 LocalPreferences.set('show_search_engine', showSearchEngine.toString());
1074 $('showRssReaderLink').addEvent('click', function(e) {
1075 showRssReader = !showRssReader;
1076 LocalPreferences.set('show_rss_reader', showRssReader.toString());
1080 $('showLogViewerLink').addEvent('click', function(e) {
1081 showLogViewer = !showLogViewer;
1082 LocalPreferences.set('show_log_viewer', showLogViewer.toString());
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)
1095 $('showRssReaderLink').firstChild.style.opacity = '0';
1096 $('rssTabLink').addClass('invisible');
1097 if ($('rssTabLink').hasClass('selected'))
1098 $("transfersTabLink").click();
1101 if (showSearchEngine) {
1102 $('showSearchEngineLink').firstChild.style.opacity = '1';
1103 $('mainWindowTabs').removeClass('invisible');
1104 $('searchTabLink').removeClass('invisible');
1105 if (!MochaUI.Panels.instances.SearchPanel)
1109 $('showSearchEngineLink').firstChild.style.opacity = '0';
1110 $('searchTabLink').addClass('invisible');
1111 if ($('searchTabLink').hasClass('selected'))
1112 $("transfersTabLink").click();
1115 if (showLogViewer) {
1116 $('showLogViewerLink').firstChild.style.opacity = '1';
1117 $('mainWindowTabs').removeClass('invisible');
1118 $('logTabLink').removeClass('invisible');
1119 if (!MochaUI.Panels.instances.LogPanel)
1123 $('showLogViewerLink').firstChild.style.opacity = '0';
1124 $('logTabLink').addClass('invisible');
1125 if ($('logTabLink').hasClass('selected'))
1126 $("transfersTabLink").click();
1130 if (!showRssReader && !showSearchEngine && !showLogViewer)
1131 $('mainWindowTabs').addClass('invisible');
1134 $('StatisticsLink').addEvent('click', StatisticsLinkFN);
1138 const showTransfersTab = function() {
1139 $("filtersColumn").removeClass("invisible");
1140 $("filtersColumn_handle").removeClass("invisible");
1141 $("mainColumn").removeClass("invisible");
1142 $('torrentsFilterToolbar').removeClass("invisible");
1144 customSyncMainDataInterval = null;
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();
1160 const showSearchTab = (function() {
1161 let searchTabInitialized = false;
1164 if (!searchTabInitialized) {
1165 window.qBittorrent.Search.init();
1166 searchTabInitialized = true;
1169 $("searchTabColumn").removeClass("invisible");
1170 customSyncMainDataInterval = 30000;
1177 const hideSearchTab = function() {
1178 $("searchTabColumn").addClass("invisible");
1179 MochaUI.Desktop.resizePanels();
1182 const showRssTab = (function() {
1183 let rssTabInitialized = false;
1186 if (!rssTabInitialized) {
1187 window.qBittorrent.Rss.init();
1188 rssTabInitialized = true;
1191 window.qBittorrent.Rss.load();
1194 $("rssTabColumn").removeClass("invisible");
1195 customSyncMainDataInterval = 30000;
1202 const hideRssTab = function() {
1203 $("rssTabColumn").addClass("invisible");
1204 window.qBittorrent.Rss && window.qBittorrent.Rss.unload();
1205 MochaUI.Desktop.resizePanels();
1208 const showLogTab = (function() {
1209 let logTabInitialized = false;
1212 if (!logTabInitialized) {
1213 window.qBittorrent.Log.init();
1214 logTabInitialized = true;
1217 window.qBittorrent.Log.load();
1220 $('logTabColumn').removeClass('invisible');
1221 customSyncMainDataInterval = 30000;
1228 const hideLogTab = function() {
1229 $('logTabColumn').addClass('invisible');
1230 MochaUI.Desktop.resizePanels();
1231 window.qBittorrent.Log && window.qBittorrent.Log.unload();
1234 const addSearchPanel = function() {
1246 contentURL: 'views/search.html',
1248 column: 'searchTabColumn',
1253 const addRssPanel = function() {
1265 contentURL: 'views/rss.html',
1267 column: 'rssTabColumn',
1272 var addLogPanel = function() {
1284 contentURL: 'views/log.html',
1286 css: ['css/vanillaSelectBox.css'],
1287 js: ['scripts/lib/vanillaSelectBox.js'],
1289 tabsURL: 'views/logTabs.html',
1290 tabsOnload: function() {
1291 MochaUI.initializeTabs('panelTabs');
1293 $('logMessageLink').addEvent('click', function(e) {
1294 window.qBittorrent.Log.setCurrentTab('main');
1297 $('logPeerLink').addEvent('click', function(e) {
1298 window.qBittorrent.Log.setCurrentTab('peer');
1303 column: 'logTabColumn',
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)
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]);
1331 contentURL: 'views/transferlist.html',
1332 onContentLoaded: function() {
1333 handleDownloadParam();
1336 column: 'mainColumn',
1337 onResize: saveColumnSizes,
1340 let prop_h = LocalPreferences.get('properties_height_rel');
1341 if ($defined(prop_h))
1342 prop_h = prop_h.toFloat() * Window.getSize().y;
1344 prop_h = Window.getSize().y / 2.0;
1346 id: 'propertiesPanel',
1355 contentURL: 'views/properties.html',
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'],
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();
1369 else if (!$('prop_trackers').hasClass('invisible')) {
1370 if (window.qBittorrent.PropTrackers !== undefined)
1371 window.qBittorrent.PropTrackers.updateData();
1373 else if (!$('prop_peers').hasClass('invisible')) {
1374 if (window.qBittorrent.PropPeers !== undefined)
1375 window.qBittorrent.PropPeers.updateData();
1377 else if (!$('prop_webseeds').hasClass('invisible')) {
1378 if (window.qBittorrent.PropWebseeds !== undefined)
1379 window.qBittorrent.PropWebseeds.updateData();
1381 else if (!$('prop_files').hasClass('invisible')) {
1382 if (window.qBittorrent.PropFiles !== undefined)
1383 window.qBittorrent.PropFiles.updateData();
1387 $('PropGeneralLink').addEvent('click', function(e) {
1388 $$('.propertiesTabContent').addClass('invisible');
1389 $('prop_general').removeClass("invisible");
1391 updatePropertiesPanel();
1392 LocalPreferences.set('selected_tab', this.id);
1395 $('PropTrackersLink').addEvent('click', function(e) {
1396 $$('.propertiesTabContent').addClass('invisible');
1397 $('prop_trackers').removeClass("invisible");
1399 updatePropertiesPanel();
1400 LocalPreferences.set('selected_tab', this.id);
1403 $('PropPeersLink').addEvent('click', function(e) {
1404 $$('.propertiesTabContent').addClass('invisible');
1405 $('prop_peers').removeClass("invisible");
1407 updatePropertiesPanel();
1408 LocalPreferences.set('selected_tab', this.id);
1411 $('PropWebSeedsLink').addEvent('click', function(e) {
1412 $$('.propertiesTabContent').addClass('invisible');
1413 $('prop_webseeds').removeClass("invisible");
1415 updatePropertiesPanel();
1416 LocalPreferences.set('selected_tab', this.id);
1419 $('PropFilesLink').addEvent('click', function(e) {
1420 $$('.propertiesTabContent').addClass('invisible');
1421 $('prop_files').removeClass("invisible");
1423 updatePropertiesPanel();
1424 LocalPreferences.set('selected_tab', this.id);
1427 $('propertiesPanel_collapseToggle').addEvent('click', function(e) {
1428 updatePropertiesPanel();
1431 column: 'mainColumn',
1435 const showFilesFilter = function() {
1436 $('torrentFilesFilterToolbar').removeClass("invisible");
1439 const hideFilesFilter = function() {
1440 $('torrentFilesFilterToolbar').addClass("invisible");
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);
1457 $('transfersTabLink').addEvent('click', showTransfersTab);
1458 $('searchTabLink').addEvent('click', showSearchTab);
1459 $('rssTabLink').addEvent('click', showRssTab);
1460 $('logTabLink').addEvent('click', showLogTab);
1463 const registerDragAndDrop = () => {
1464 $('desktop').addEventListener('dragover', (ev) => {
1465 if (ev.preventDefault)
1466 ev.preventDefault();
1469 $('desktop').addEventListener('dragenter', (ev) => {
1470 if (ev.preventDefault)
1471 ev.preventDefault();
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)
1490 const id = 'uploadPage';
1491 new MochaUI.Window({
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
1500 paddingHorizontal: 0,
1501 width: loadWindowWidth(id, 500),
1502 height: loadWindowHeight(id, 460),
1506 onContentLoaded: () => {
1507 const fileInput = $(`${id}_iframe`).contentDocument.getElementById('fileselect');
1508 fileInput.files = droppedFiles;
1513 const droppedText = ev.dataTransfer.getData("text");
1514 if (droppedText.length > 0) {
1517 const urls = droppedText.split('\n')
1518 .map((str) => str.trim())
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
1528 if (urls.length <= 0)
1531 const id = 'downloadPage';
1532 const contentURI = new URI('download.html').setData("urls", urls.map(encodeURIComponent).join("|"));
1533 new MochaUI.Window({
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
1543 paddingHorizontal: 0,
1544 width: loadWindowWidth(id, 500),
1545 height: loadWindowHeight(id, 600),
1553 registerDragAndDrop();
1556 defaultEventType: 'keydown',
1558 'ctrl+a': function(event) {
1559 if (event.target.nodeName == "INPUT" || event.target.nodeName == "TEXTAREA")
1561 if (event.target.isContentEditable)
1563 torrentsTable.selectAll();
1564 event.preventDefault();
1566 'delete': function(event) {
1567 if (event.target.nodeName == "INPUT" || event.target.nodeName == "TEXTAREA")
1569 if (event.target.isContentEditable)
1572 event.preventDefault();
1574 'shift+delete': (event) => {
1575 if (event.target.nodeName == "INPUT" || event.target.nodeName == "TEXTAREA")
1577 if (event.target.isContentEditable)
1580 event.preventDefault();
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();