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
,
44 const closeWindows = function() {
48 const genHash = function(string
) {
50 // https://stackoverflow.com/a/8831937
51 // https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0
53 for (let i
= 0; i
< string
.length
; ++i
)
54 hash
= ((Math
.imul(hash
, 31) + string
.charCodeAt(i
)) | 0);
58 const getSyncMainDataInterval = function() {
59 return customSyncMainDataInterval
? customSyncMainDataInterval
: serverSyncMainDataInterval
;
63 const isStopped
= () => {
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}` : '');
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() {};
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() {};
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'
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();
160 onResize
: saveColumnSizes
,
162 resizeLimit
: [1, 300]
171 const buildSearchTab = function() {
173 id
: 'searchTabColumn',
179 $("searchTabColumn").addClass("invisible");
182 const buildRssTab = function() {
190 $("rssTabColumn").addClass("invisible");
193 const buildLogTab = function() {
201 $('logTabColumn').addClass('invisible');
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')
218 setTagFilter = function(hash
) {
220 LocalPreferences
.set('selected_tag', selectedTag
);
221 highlightSelectedTag();
222 if (torrentsTable
.tableBody
!== undefined)
226 setTrackerFilter = function(hash
) {
227 selectedTracker
= hash
.toString();
228 LocalPreferences
.set('selected_tracker', selectedTracker
);
229 highlightSelectedTracker();
230 if (torrentsTable
.tableBody
!== undefined)
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");
252 LocalPreferences
.set('selected_filter', f
);
254 if (typeof torrentsTable
.tableBody
!= 'undefined')
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");
265 toggleIcon
[0].toggleClass("rotate");
279 contentURL
: 'views/filters.html',
280 onContentLoaded: function() {
281 setFilter(selected_filter
);
283 column
: 'filtersColumn',
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";
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
) {
330 category_list
.forEach((category
) => {
331 const deleteResult
= category
.torrents
.delete(hash
);
332 removed
||= deleteResult
;
338 const addTorrentToCategoryList = function(torrent
) {
339 const category
= torrent
['category'];
340 if (typeof category
=== 'undefined')
343 const hash
= torrent
['hash'];
344 if (category
.length
=== 0) { // Empty category
345 removeTorrentFromCategoryList(hash
);
349 const categoryHash
= window
.qBittorrent
.Client
.genHash(category
);
350 if (!category_list
.has(categoryHash
)) { // This should not happen
351 category_list
.set(categoryHash
, {
357 const torrents
= category_list
.get(categoryHash
).torrents
;
358 if (!torrents
.has(hash
)) {
359 removeTorrentFromCategoryList(hash
);
366 const removeTorrentFromTagList = function(hash
) {
371 tagList
.forEach((tag
) => {
372 const deleteResult
= tag
.torrents
.delete(hash
);
373 removed
||= deleteResult
;
379 const addTorrentToTagList = function(torrent
) {
380 if (torrent
['tags'] === undefined) // Tags haven't changed
383 const hash
= torrent
['hash'];
384 removeTorrentFromTagList(hash
);
386 if (torrent
['tags'].length
=== 0) // No tags
389 const tags
= torrent
['tags'].split(',');
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
, {
400 const torrents
= tagList
.get(tagHash
).torrents
;
401 if (!torrents
.has(hash
)) {
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');
434 categoryList
.getChildren().each(c
=> c
.destroy());
436 const create_link = function(hash
, text
, count
) {
437 let display_name
= text
;
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', {
452 window
.qBittorrent
.Filters
.categoriesFilterContextMenu
.addTarget(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
))
462 const row
= torrentsTable
.rows
[key
];
463 if (row
['full_data'].category
.length
=== 0)
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
,
473 categoryCount
: category
.torrents
.size
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');
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";
515 children
[i
].className
= "";
519 const updateTagList = function() {
520 const tagFilterList
= $('tagFilterList');
521 if (tagFilterList
=== null)
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', {
534 window
.qBittorrent
.Filters
.tagsFilterContextMenu
.addTarget(el
);
538 const torrentsCount
= torrentsTable
.getRowIds().length
;
540 for (const key
in torrentsTable
.rows
) {
541 if (Object
.hasOwn(torrentsTable
.rows
, key
) && (torrentsTable
.rows
[key
]['full_data'].tags
.length
=== 0))
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({
551 tagSize
: tag
.torrents
.size
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');
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
)) {
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
;
597 const updateTrackerList = function() {
598 const trackerFilterList
= $('trackerFilterList');
599 if (trackerFilterList
=== null)
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', {
612 window
.qBittorrent
.Filters
.trackersFilterContextMenu
.addTarget(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
);
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
)
653 const children
= trackerFilterList
.childNodes
;
654 for (const child
of children
)
655 child
.className
= (child
.id
=== selectedTracker
) ? "selectedFilter" : "";
658 const setupCopyEventHandler
= (function() {
663 clipboardEvent
.destroy();
665 clipboardEvent
= new ClipboardJS('.copyToClipboard', {
666 text: function(trigger
) {
667 switch (trigger
.id
) {
670 case "copyInfohash1":
671 return copyInfohashFN(1);
672 case "copyInfohash2":
673 return copyInfohashFN(2);
674 case "copyMagnetLink":
675 return copyMagnetLinkFN();
679 return copyCommentFN();
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({
697 onFailure: function() {
698 const errorDiv
= $('error_div');
700 errorDiv
.set('html', 'QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]');
701 syncRequestInProgress
= false;
704 onSuccess: function(response
) {
705 $('error_div').set('html', '');
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);
716 torrentsTableSelectedRows
= torrentsTable
.selectedRowsIds();
717 torrentsTable
.clear();
718 category_list
.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
))
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
;
737 category_list
.set(categoryHash
, {
738 name
: responseCategory
.name
,
739 savePath
: responseCategory
.savePath
,
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
, {
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
);
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
]))
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'];
830 serverState
[k
] = tmp
[k
];
831 processServerState();
834 if (update_categories
) {
835 updateCategoryList();
836 window
.qBittorrent
.TransferList
.contextMenu
.updateCategoriesSubMenu(category_list
);
840 window
.qBittorrent
.TransferList
.contextMenu
.updateTagsSubMenu(tagList
);
846 // re-select previously selected rows
847 torrentsTable
.reselectRows(torrentsTableSelectedRows
);
849 syncRequestInProgress
= false;
850 syncData(window
.qBittorrent
.Client
.getSyncMainDataInterval());
853 syncRequestInProgress
= true;
857 updateMainData = function() {
858 torrentsTable
.updateTable();
862 const syncData = function(delay
) {
863 if (syncRequestInProgress
)
866 clearTimeout(syncMainDataTimeoutID
);
868 if (window
.qBittorrent
.Client
.isStopped())
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)))
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
));
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
) {
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]';
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]';
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]';
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');
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
) {
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]';
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
);
983 url
: 'api/v2/transfer/toggleSpeedLimitsMode',
985 onComplete: function() {
986 alternativeSpeedLimits
= !alternativeSpeedLimits
;
989 onFailure: function() {
990 // Restore icon in case of failure
991 updateAltSpeedIcon(alternativeSpeedLimits
);
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');
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');
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]");
1032 alert("QBT_TR(Your browser does not support this feature)QBT_TR[CONTEXT=MainWindow]");
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');
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());
1071 $('speedInBrowserTitleBarLink').firstChild
.style
.opacity
= '1';
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());
1083 $('showRssReaderLink').addEvent('click', function(e
) {
1084 showRssReader
= !showRssReader
;
1085 LocalPreferences
.set('show_rss_reader', showRssReader
.toString());
1089 $('showLogViewerLink').addEvent('click', function(e
) {
1090 showLogViewer
= !showLogViewer
;
1091 LocalPreferences
.set('show_log_viewer', showLogViewer
.toString());
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
)
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
)
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
)
1132 $('showLogViewerLink').firstChild
.style
.opacity
= '0';
1133 $('logTabLink').addClass('invisible');
1134 if ($('logTabLink').hasClass('selected'))
1135 $("transfersTabLink").click();
1139 if (!showRssReader
&& !showSearchEngine
&& !showLogViewer
)
1140 $('mainWindowTabs').addClass('invisible');
1143 $('StatisticsLink').addEvent('click', StatisticsLinkFN
);
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;
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;
1176 if (!searchTabInitialized
) {
1177 window
.qBittorrent
.Search
.init();
1178 searchTabInitialized
= true;
1181 $("searchTabColumn").removeClass("invisible");
1182 customSyncMainDataInterval
= 30000;
1189 const hideSearchTab = function() {
1190 $("searchTabColumn").addClass("invisible");
1191 MochaUI
.Desktop
.resizePanels();
1194 const showRssTab
= (function() {
1195 let rssTabInitialized
= false;
1198 if (!rssTabInitialized
) {
1199 window
.qBittorrent
.Rss
.init();
1200 rssTabInitialized
= true;
1203 window
.qBittorrent
.Rss
.load();
1206 $("rssTabColumn").removeClass("invisible");
1207 customSyncMainDataInterval
= 30000;
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;
1224 if (!logTabInitialized
) {
1225 window
.qBittorrent
.Log
.init();
1226 logTabInitialized
= true;
1229 window
.qBittorrent
.Log
.load();
1232 $('logTabColumn').removeClass('invisible');
1233 customSyncMainDataInterval
= 30000;
1240 const hideLogTab = function() {
1241 $('logTabColumn').addClass('invisible');
1242 MochaUI
.Desktop
.resizePanels();
1243 window
.qBittorrent
.Log
&& window
.qBittorrent
.Log
.unload();
1246 const addSearchPanel = function() {
1258 contentURL
: 'views/search.html',
1260 column
: 'searchTabColumn',
1265 const addRssPanel = function() {
1277 contentURL
: 'views/rss.html',
1279 column
: 'rssTabColumn',
1284 var addLogPanel = function() {
1296 contentURL
: 'views/log.html',
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');
1315 column
: 'logTabColumn',
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)
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
]);
1343 contentURL
: 'views/transferlist.html',
1344 onContentLoaded: function() {
1345 handleDownloadParam();
1348 column
: 'mainColumn',
1349 onResize
: saveColumnSizes
,
1352 let prop_h
= LocalPreferences
.get('properties_height_rel');
1353 if ($defined(prop_h
))
1354 prop_h
= prop_h
.toFloat() * Window
.getSize().y
;
1356 prop_h
= Window
.getSize().y
/ 2.0;
1358 id
: 'propertiesPanel',
1367 contentURL
: 'views/properties.html',
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");
1403 updatePropertiesPanel();
1404 LocalPreferences
.set('selected_tab', this.id
);
1407 $('PropTrackersLink').addEvent('click', function(e
) {
1408 $$('.propertiesTabContent').addClass('invisible');
1409 $('prop_trackers').removeClass("invisible");
1411 updatePropertiesPanel();
1412 LocalPreferences
.set('selected_tab', this.id
);
1415 $('PropPeersLink').addEvent('click', function(e
) {
1416 $$('.propertiesTabContent').addClass('invisible');
1417 $('prop_peers').removeClass("invisible");
1419 updatePropertiesPanel();
1420 LocalPreferences
.set('selected_tab', this.id
);
1423 $('PropWebSeedsLink').addEvent('click', function(e
) {
1424 $$('.propertiesTabContent').addClass('invisible');
1425 $('prop_webseeds').removeClass("invisible");
1427 updatePropertiesPanel();
1428 LocalPreferences
.set('selected_tab', this.id
);
1431 $('PropFilesLink').addEvent('click', function(e
) {
1432 $$('.propertiesTabContent').addClass('invisible');
1433 $('prop_files').removeClass("invisible");
1435 updatePropertiesPanel();
1436 LocalPreferences
.set('selected_tab', this.id
);
1439 $('propertiesPanel_collapseToggle').addEvent('click', function(e
) {
1440 updatePropertiesPanel();
1443 column
: 'mainColumn',
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
);
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
)
1501 const id
= 'uploadPage';
1502 new MochaUI
.Window({
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
1511 paddingHorizontal
: 0,
1512 width
: loadWindowWidth(id
, 500),
1513 height
: loadWindowHeight(id
, 460),
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) {
1528 const urls
= droppedText
.split('\n')
1529 .map((str
) => str
.trim())
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)
1542 const id
= 'downloadPage';
1543 const contentURI
= new URI('download.html').setData("urls", urls
.map(encodeURIComponent
).join("|"));
1544 new MochaUI
.Window({
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
1554 paddingHorizontal
: 0,
1555 width
: loadWindowWidth(id
, 500),
1556 height
: loadWindowHeight(id
, 600),
1564 registerDragAndDrop();
1567 defaultEventType
: 'keydown',
1569 'ctrl+a': function(event
) {
1570 if (event
.target
.nodeName
== "INPUT" || event
.target
.nodeName
== "TEXTAREA")
1572 if (event
.target
.isContentEditable
)
1574 torrentsTable
.selectAll();
1575 event
.preventDefault();
1577 'delete': function(event
) {
1578 if (event
.target
.nodeName
== "INPUT" || event
.target
.nodeName
== "TEXTAREA")
1580 if (event
.target
.isContentEditable
)
1583 event
.preventDefault();
1585 'shift+delete': (event
) => {
1586 if (event
.target
.nodeName
== "INPUT" || event
.target
.nodeName
== "TEXTAREA")
1588 if (event
.target
.isContentEditable
)
1591 event
.preventDefault();
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();