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 window
.qBittorrent
??= {};
29 window
.qBittorrent
.Client
??= (() => {
30 const exports
= () => {
32 closeWindows
: closeWindows
,
34 getSyncMainDataInterval
: getSyncMainDataInterval
,
38 showSearchEngine
: showSearchEngine
,
39 showRssReader
: showRssReader
,
40 showLogViewer
: showLogViewer
,
41 isShowSearchEngine
: isShowSearchEngine
,
42 isShowRssReader
: isShowRssReader
,
43 isShowLogViewer
: isShowLogViewer
47 const closeWindows = function() {
51 const genHash = function(string
) {
53 // https://stackoverflow.com/a/8831937
54 // https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0
56 for (let i
= 0; i
< string
.length
; ++i
)
57 hash
= ((Math
.imul(hash
, 31) + string
.charCodeAt(i
)) | 0);
61 const getSyncMainDataInterval = function() {
62 return customSyncMainDataInterval
? customSyncMainDataInterval
: serverSyncMainDataInterval
;
66 const isStopped
= () => {
74 const mainTitle
= () => {
75 const emDash
= "\u2014";
76 const qbtVersion
= window
.qBittorrent
.Cache
.qbtVersion
.get();
77 const suffix
= window
.qBittorrent
.Cache
.preferences
.get()["app_instance_name"] || "";
78 const title
= `qBittorrent ${qbtVersion} QBT_TR(WebUI)QBT_TR[CONTEXT=OptionsDialog]`
79 + ((suffix
.length
> 0) ? ` ${emDash} ${suffix}` : "");
83 let showingSearchEngine
= false;
84 let showingRssReader
= false;
85 let showingLogViewer
= false;
87 const showSearchEngine = function(bool
) {
88 showingSearchEngine
= bool
;
90 const showRssReader = function(bool
) {
91 showingRssReader
= bool
;
93 const showLogViewer = function(bool
) {
94 showingLogViewer
= bool
;
96 const isShowSearchEngine = function() {
97 return showingSearchEngine
;
99 const isShowRssReader = function() {
100 return showingRssReader
;
102 const isShowLogViewer = function() {
103 return showingLogViewer
;
108 Object
.freeze(window
.qBittorrent
.Client
);
110 // TODO: move global functions/variables into some namespace/scope
112 this.torrentsTable
= new window
.qBittorrent
.DynamicTable
.TorrentsTable();
114 let updatePropertiesPanel = function() {};
116 this.updateMainData = function() {};
117 let alternativeSpeedLimits
= false;
118 let queueing_enabled
= true;
119 let serverSyncMainDataInterval
= 1500;
120 let customSyncMainDataInterval
= null;
121 let useSubcategories
= true;
123 /* Categories filter */
124 const CATEGORIES_ALL
= 1;
125 const CATEGORIES_UNCATEGORIZED
= 2;
127 const category_list
= new Map();
129 let selected_category
= Number(LocalPreferences
.get("selected_category", CATEGORIES_ALL
));
130 let setCategoryFilter = function() {};
134 const TAGS_UNTAGGED
= 2;
136 const tagList
= new Map();
138 let selectedTag
= Number(LocalPreferences
.get("selected_tag", TAGS_ALL
));
139 let setTagFilter = function() {};
141 /* Trackers filter */
142 const TRACKERS_ALL
= 1;
143 const TRACKERS_TRACKERLESS
= 2;
145 /** @type Map<number, {host: string, trackerTorrentMap: Map<string, string[]>}> **/
146 const trackerList
= new Map();
148 let selectedTracker
= LocalPreferences
.get("selected_tracker", TRACKERS_ALL
);
149 let setTrackerFilter = function() {};
152 let selected_filter
= LocalPreferences
.get("selected_filter", "all");
153 let setFilter = function() {};
154 let toggleFilterDisplay = function() {};
156 window
.addEventListener("DOMContentLoaded", () => {
157 let isSearchPanelLoaded
= false;
158 let isLogPanelLoaded
= false;
160 const saveColumnSizes = function() {
161 const filters_width
= $("Filters").getSize().x
;
162 LocalPreferences
.set("filters_width", filters_width
);
163 const properties_height_rel
= $("propertiesPanel").getSize().y
/ Window
.getSize().y
;
164 LocalPreferences
.set("properties_height_rel", properties_height_rel
);
167 window
.addEvent("resize", () => {
168 // only save sizes if the columns are visible
169 if (!$("mainColumn").hasClass("invisible"))
170 saveColumnSizes
.delay(200); // Resizing might takes some time.
173 /* MochaUI.Desktop = new MochaUI.Desktop();
174 MochaUI.Desktop.desktop.setStyles({
175 'background': '#fff',
176 'visibility': 'visible'
178 MochaUI
.Desktop
.initialize();
180 const buildTransfersTab = function() {
181 const filt_w
= Number(LocalPreferences
.get("filters_width", 120));
185 onResize
: saveColumnSizes
,
187 resizeLimit
: [1, 300]
195 const buildSearchTab = function() {
197 id
: "searchTabColumn",
203 $("searchTabColumn").addClass("invisible");
206 const buildRssTab = function() {
214 $("rssTabColumn").addClass("invisible");
217 const buildLogTab = function() {
225 $("logTabColumn").addClass("invisible");
232 MochaUI
.initializeTabs("mainWindowTabsList");
234 setCategoryFilter = function(hash
) {
235 selected_category
= hash
;
236 LocalPreferences
.set("selected_category", selected_category
);
237 highlightSelectedCategory();
238 if (typeof torrentsTable
.tableBody
!== "undefined")
242 setTagFilter = function(hash
) {
244 LocalPreferences
.set("selected_tag", selectedTag
);
245 highlightSelectedTag();
246 if (torrentsTable
.tableBody
!== undefined)
250 setTrackerFilter = function(hash
) {
251 selectedTracker
= hash
.toString();
252 LocalPreferences
.set("selected_tracker", selectedTracker
);
253 highlightSelectedTracker();
254 if (torrentsTable
.tableBody
!== undefined)
258 setFilter = function(f
) {
259 // Visually Select the right filter
260 $("all_filter").removeClass("selectedFilter");
261 $("downloading_filter").removeClass("selectedFilter");
262 $("seeding_filter").removeClass("selectedFilter");
263 $("completed_filter").removeClass("selectedFilter");
264 $("stopped_filter").removeClass("selectedFilter");
265 $("running_filter").removeClass("selectedFilter");
266 $("active_filter").removeClass("selectedFilter");
267 $("inactive_filter").removeClass("selectedFilter");
268 $("stalled_filter").removeClass("selectedFilter");
269 $("stalled_uploading_filter").removeClass("selectedFilter");
270 $("stalled_downloading_filter").removeClass("selectedFilter");
271 $("checking_filter").removeClass("selectedFilter");
272 $("moving_filter").removeClass("selectedFilter");
273 $("errored_filter").removeClass("selectedFilter");
274 $(f
+ "_filter").addClass("selectedFilter");
276 LocalPreferences
.set("selected_filter", f
);
278 if (typeof torrentsTable
.tableBody
!== "undefined")
282 toggleFilterDisplay = function(filter
) {
283 const element
= filter
+ "FilterList";
284 LocalPreferences
.set("filter_" + filter
+ "_collapsed", !$(element
).hasClass("invisible"));
285 $(element
).toggleClass("invisible");
286 const parent
= $(element
).getParent(".filterWrapper");
287 const toggleIcon
= $(parent
).getChildren(".filterTitle img");
289 toggleIcon
[0].toggleClass("rotate");
303 contentURL
: "views/filters.html",
304 onContentLoaded: function() {
305 setFilter(selected_filter
);
307 column
: "filtersColumn",
312 // Show Top Toolbar is enabled by default
313 let showTopToolbar
= LocalPreferences
.get("show_top_toolbar", "true") === "true";
314 if (!showTopToolbar
) {
315 $("showTopToolbarLink").firstChild
.style
.opacity
= "0";
316 $("mochaToolbar").addClass("invisible");
319 // Show Status Bar is enabled by default
320 let showStatusBar
= LocalPreferences
.get("show_status_bar", "true") === "true";
321 if (!showStatusBar
) {
322 $("showStatusBarLink").firstChild
.style
.opacity
= "0";
323 $("desktopFooterWrapper").addClass("invisible");
326 // Show Filters Sidebar is enabled by default
327 let showFiltersSidebar
= LocalPreferences
.get("show_filters_sidebar", "true") === "true";
328 if (!showFiltersSidebar
) {
329 $("showFiltersSidebarLink").firstChild
.style
.opacity
= "0";
330 $("filtersColumn").addClass("invisible");
331 $("filtersColumn_handle").addClass("invisible");
334 let speedInTitle
= LocalPreferences
.get("speed_in_browser_title_bar") === "true";
336 $("speedInBrowserTitleBarLink").firstChild
.style
.opacity
= "0";
338 // After showing/hiding the toolbar + status bar
339 window
.qBittorrent
.Client
.showSearchEngine(LocalPreferences
.get("show_search_engine") !== "false");
340 window
.qBittorrent
.Client
.showRssReader(LocalPreferences
.get("show_rss_reader") !== "false");
341 window
.qBittorrent
.Client
.showLogViewer(LocalPreferences
.get("show_log_viewer") === "true");
343 // After Show Top Toolbar
344 MochaUI
.Desktop
.setDesktopSize();
346 let syncMainDataLastResponseId
= 0;
347 const serverState
= {};
349 const removeTorrentFromCategoryList = function(hash
) {
354 category_list
.forEach((category
) => {
355 const deleteResult
= category
.torrents
.delete(hash
);
356 removed
||= deleteResult
;
362 const addTorrentToCategoryList = function(torrent
) {
363 const category
= torrent
["category"];
364 if (typeof category
=== "undefined")
367 const hash
= torrent
["hash"];
368 if (category
.length
=== 0) { // Empty category
369 removeTorrentFromCategoryList(hash
);
373 const categoryHash
= window
.qBittorrent
.Client
.genHash(category
);
374 if (!category_list
.has(categoryHash
)) { // This should not happen
375 category_list
.set(categoryHash
, {
381 const torrents
= category_list
.get(categoryHash
).torrents
;
382 if (!torrents
.has(hash
)) {
383 removeTorrentFromCategoryList(hash
);
390 const removeTorrentFromTagList = function(hash
) {
395 tagList
.forEach((tag
) => {
396 const deleteResult
= tag
.torrents
.delete(hash
);
397 removed
||= deleteResult
;
403 const addTorrentToTagList = function(torrent
) {
404 if (torrent
["tags"] === undefined) // Tags haven't changed
407 const hash
= torrent
["hash"];
408 removeTorrentFromTagList(hash
);
410 if (torrent
["tags"].length
=== 0) // No tags
413 const tags
= torrent
["tags"].split(",");
415 for (let i
= 0; i
< tags
.length
; ++i
) {
416 const tagHash
= window
.qBittorrent
.Client
.genHash(tags
[i
].trim());
417 if (!tagList
.has(tagHash
)) { // This should not happen
418 tagList
.set(tagHash
, {
424 const torrents
= tagList
.get(tagHash
).torrents
;
425 if (!torrents
.has(hash
)) {
433 const updateFilter = function(filter
, filterTitle
) {
434 $(filter
+ "_filter").firstChild
.childNodes
[1].nodeValue
= filterTitle
.replace("%1", torrentsTable
.getFilteredTorrentsNumber(filter
, CATEGORIES_ALL
, TAGS_ALL
, TRACKERS_ALL
));
437 const updateFiltersList = function() {
438 updateFilter("all", "QBT_TR(All (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
439 updateFilter("downloading", "QBT_TR(Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
440 updateFilter("seeding", "QBT_TR(Seeding (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
441 updateFilter("completed", "QBT_TR(Completed (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
442 updateFilter("running", "QBT_TR(Running (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
443 updateFilter("stopped", "QBT_TR(Stopped (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
444 updateFilter("active", "QBT_TR(Active (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
445 updateFilter("inactive", "QBT_TR(Inactive (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
446 updateFilter("stalled", "QBT_TR(Stalled (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
447 updateFilter("stalled_uploading", "QBT_TR(Stalled Uploading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
448 updateFilter("stalled_downloading", "QBT_TR(Stalled Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
449 updateFilter("checking", "QBT_TR(Checking (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
450 updateFilter("moving", "QBT_TR(Moving (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
451 updateFilter("errored", "QBT_TR(Errored (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
454 const updateCategoryList = function() {
455 const categoryList
= $("categoryFilterList");
458 categoryList
.getChildren().each(c
=> c
.destroy());
460 const create_link = function(hash
, text
, count
) {
461 let display_name
= text
;
463 if (useSubcategories
) {
464 const category_path
= text
.split("/");
465 display_name
= category_path
[category_path
.length
- 1];
466 margin_left
= (category_path
.length
- 1) * 20;
469 const html
= `<span class="link" href="#" style="margin-left: ${margin_left}px;" onclick="setCategoryFilter(${hash}); return false;">`
470 + '<img src="images/view-categories.svg"/>'
471 + window
.qBittorrent
.Misc
.escapeHtml(display_name
) + " (" + count
+ ")" + "</span>";
472 const el
= new Element("li", {
476 window
.qBittorrent
.Filters
.categoriesFilterContextMenu
.addTarget(el
);
480 const all
= torrentsTable
.getRowIds().length
;
481 let uncategorized
= 0;
482 for (const key
in torrentsTable
.rows
) {
483 if (!Object
.hasOwn(torrentsTable
.rows
, key
))
486 const row
= torrentsTable
.rows
[key
];
487 if (row
["full_data"].category
.length
=== 0)
490 categoryList
.appendChild(create_link(CATEGORIES_ALL
, "QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]", all
));
491 categoryList
.appendChild(create_link(CATEGORIES_UNCATEGORIZED
, "QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]", uncategorized
));
493 const sortedCategories
= [];
494 category_list
.forEach((category
, hash
) => sortedCategories
.push({
495 categoryName
: category
.name
,
497 categoryCount
: category
.torrents
.size
499 sortedCategories
.sort((left
, right
) => {
500 const leftSegments
= left
.categoryName
.split("/");
501 const rightSegments
= right
.categoryName
.split("/");
503 for (let i
= 0, iMax
= Math
.min(leftSegments
.length
, rightSegments
.length
); i
< iMax
; ++i
) {
504 const compareResult
= window
.qBittorrent
.Misc
.naturalSortCollator
.compare(
505 leftSegments
[i
], rightSegments
[i
]);
506 if (compareResult
!== 0)
507 return compareResult
;
510 return leftSegments
.length
- rightSegments
.length
;
513 for (let i
= 0; i
< sortedCategories
.length
; ++i
) {
514 const { categoryName
, categoryHash
} = sortedCategories
[i
];
515 let { categoryCount
} = sortedCategories
[i
];
517 if (useSubcategories
) {
518 for (let j
= (i
+ 1);
519 ((j
< sortedCategories
.length
) && sortedCategories
[j
].categoryName
.startsWith(categoryName
+ "/")); ++j
)
520 categoryCount
+= sortedCategories
[j
].categoryCount
;
523 categoryList
.appendChild(create_link(categoryHash
, categoryName
, categoryCount
));
526 highlightSelectedCategory();
529 const highlightSelectedCategory = function() {
530 const categoryList
= $("categoryFilterList");
533 const children
= categoryList
.childNodes
;
534 for (let i
= 0; i
< children
.length
; ++i
) {
535 if (Number(children
[i
].id
) === selected_category
)
536 children
[i
].className
= "selectedFilter";
538 children
[i
].className
= "";
542 const updateTagList = function() {
543 const tagFilterList
= $("tagFilterList");
544 if (tagFilterList
=== null)
547 tagFilterList
.getChildren().each(c
=> c
.destroy());
549 const createLink = function(hash
, text
, count
) {
550 const html
= `<span class="link" href="#" onclick="setTagFilter(${hash}); return false;">`
551 + '<img src="images/tags.svg"/>'
552 + window
.qBittorrent
.Misc
.escapeHtml(text
) + " (" + count
+ ")" + "</span>";
553 const el
= new Element("li", {
557 window
.qBittorrent
.Filters
.tagsFilterContextMenu
.addTarget(el
);
561 const torrentsCount
= torrentsTable
.getRowIds().length
;
563 for (const key
in torrentsTable
.rows
) {
564 if (Object
.hasOwn(torrentsTable
.rows
, key
) && (torrentsTable
.rows
[key
]["full_data"].tags
.length
=== 0))
567 tagFilterList
.appendChild(createLink(TAGS_ALL
, "QBT_TR(All)QBT_TR[CONTEXT=TagFilterModel]", torrentsCount
));
568 tagFilterList
.appendChild(createLink(TAGS_UNTAGGED
, "QBT_TR(Untagged)QBT_TR[CONTEXT=TagFilterModel]", untagged
));
570 const sortedTags
= [];
571 tagList
.forEach((tag
, hash
) => sortedTags
.push({
574 tagSize
: tag
.torrents
.size
576 sortedTags
.sort((left
, right
) => window
.qBittorrent
.Misc
.naturalSortCollator
.compare(left
.tagName
, right
.tagName
));
578 for (const { tagName
, tagHash
, tagSize
} of sortedTags
)
579 tagFilterList
.appendChild(createLink(tagHash
, tagName
, tagSize
));
581 highlightSelectedTag();
584 const highlightSelectedTag = function() {
585 const tagFilterList
= $("tagFilterList");
589 const children
= tagFilterList
.childNodes
;
590 for (let i
= 0; i
< children
.length
; ++i
)
591 children
[i
].className
= (Number(children
[i
].id
) === selectedTag
) ? "selectedFilter" : "";
594 // getHost emulate the GUI version `QString getHost(const QString &url)`
595 const getHost = function(url
) {
596 // We want the hostname.
597 // If failed to parse the domain, original input should be returned
599 if (!/^(?:https?|udp):/i.test(url
))
603 // hack: URL can not get hostname from udp protocol
604 const parsedUrl
= new URL(url
.replace(/^udp:/i, "https:"));
605 // host: "example.com:8443"
606 // hostname: "example.com"
607 const host
= parsedUrl
.hostname
;
618 const updateTrackerList = function() {
619 const trackerFilterList
= $("trackerFilterList");
620 if (trackerFilterList
=== null)
623 trackerFilterList
.getChildren().each(c
=> c
.destroy());
625 const createLink = function(hash
, text
, count
) {
626 const html
= '<span class="link" href="#" onclick="setTrackerFilter(' + hash
+ ');return false;">'
627 + '<img src="images/trackers.svg"/>'
628 + window
.qBittorrent
.Misc
.escapeHtml(text
.replace("%1", count
)) + "</span>";
629 const el
= new Element("li", {
633 window
.qBittorrent
.Filters
.trackersFilterContextMenu
.addTarget(el
);
637 const torrentsCount
= torrentsTable
.getRowIds().length
;
638 trackerFilterList
.appendChild(createLink(TRACKERS_ALL
, "QBT_TR(All (%1))QBT_TR[CONTEXT=TrackerFiltersList]", torrentsCount
));
639 let trackerlessTorrentsCount
= 0;
640 for (const key
in torrentsTable
.rows
) {
641 if (Object
.hasOwn(torrentsTable
.rows
, key
) && (torrentsTable
.rows
[key
]["full_data"].trackers_count
=== 0))
642 trackerlessTorrentsCount
+= 1;
644 trackerFilterList
.appendChild(createLink(TRACKERS_TRACKERLESS
, "QBT_TR(Trackerless (%1))QBT_TR[CONTEXT=TrackerFiltersList]", trackerlessTorrentsCount
));
646 // Sort trackers by hostname
647 const sortedList
= [];
648 trackerList
.forEach(({ host
, trackerTorrentMap
}, hash
) => {
649 const uniqueTorrents
= new Set();
650 for (const torrents
of trackerTorrentMap
.values()) {
651 for (const torrent
of torrents
)
652 uniqueTorrents
.add(torrent
);
658 trackerCount
: uniqueTorrents
.size
,
661 sortedList
.sort((left
, right
) => window
.qBittorrent
.Misc
.naturalSortCollator
.compare(left
.trackerHost
, right
.trackerHost
));
662 for (const { trackerHost
, trackerHash
, trackerCount
} of sortedList
)
663 trackerFilterList
.appendChild(createLink(trackerHash
, (trackerHost
+ " (%1)"), trackerCount
));
665 highlightSelectedTracker();
668 const highlightSelectedTracker = function() {
669 const trackerFilterList
= $("trackerFilterList");
670 if (!trackerFilterList
)
673 const children
= trackerFilterList
.childNodes
;
674 for (const child
of children
)
675 child
.className
= (child
.id
=== selectedTracker
) ? "selectedFilter" : "";
678 const setupCopyEventHandler
= (function() {
683 clipboardEvent
.destroy();
685 clipboardEvent
= new ClipboardJS(".copyToClipboard", {
686 text: function(trigger
) {
687 switch (trigger
.id
) {
690 case "copyInfohash1":
691 return copyInfohashFN(1);
692 case "copyInfohash2":
693 return copyInfohashFN(2);
694 case "copyMagnetLink":
695 return copyMagnetLinkFN();
699 return copyCommentFN();
708 let syncMainDataTimeoutID
;
709 let syncRequestInProgress
= false;
710 const syncMainData = function() {
711 const url
= new URI("api/v2/sync/maindata");
712 url
.setData("rid", syncMainDataLastResponseId
);
713 const request
= new Request
.JSON({
717 onFailure: function() {
718 const errorDiv
= $("error_div");
720 errorDiv
.textContent
= "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]";
721 syncRequestInProgress
= false;
724 onSuccess: function(response
) {
725 $("error_div").textContent
= "";
727 clearTimeout(torrentsFilterInputTimer
);
728 torrentsFilterInputTimer
= -1;
730 let torrentsTableSelectedRows
;
731 let update_categories
= false;
732 let updateTags
= false;
733 let updateTrackers
= false;
734 const full_update
= (response
["full_update"] === true);
736 torrentsTableSelectedRows
= torrentsTable
.selectedRowsIds();
737 torrentsTable
.clear();
738 category_list
.clear();
742 syncMainDataLastResponseId
= response
["rid"];
743 if (response
["categories"]) {
744 for (const key
in response
["categories"]) {
745 if (!Object
.hasOwn(response
["categories"], key
))
748 const responseCategory
= response
["categories"][key
];
749 const categoryHash
= window
.qBittorrent
.Client
.genHash(key
);
750 const category
= category_list
.get(categoryHash
);
751 if (category
!== undefined) {
752 // only the save path can change for existing categories
753 category
.savePath
= responseCategory
.savePath
;
756 category_list
.set(categoryHash
, {
757 name
: responseCategory
.name
,
758 savePath
: responseCategory
.savePath
,
763 update_categories
= true;
765 if (response
["categories_removed"]) {
766 response
["categories_removed"].each((category
) => {
767 const categoryHash
= window
.qBittorrent
.Client
.genHash(category
);
768 category_list
.delete(categoryHash
);
770 update_categories
= true;
772 if (response
["tags"]) {
773 for (const tag
of response
["tags"]) {
774 const tagHash
= window
.qBittorrent
.Client
.genHash(tag
);
775 if (!tagList
.has(tagHash
)) {
776 tagList
.set(tagHash
, {
784 if (response
["tags_removed"]) {
785 for (let i
= 0; i
< response
["tags_removed"].length
; ++i
) {
786 const tagHash
= window
.qBittorrent
.Client
.genHash(response
["tags_removed"][i
]);
787 tagList
.delete(tagHash
);
791 if (response
["trackers"]) {
792 for (const [tracker
, torrents
] of Object
.entries(response
["trackers"])) {
793 const host
= getHost(tracker
);
794 const hash
= window
.qBittorrent
.Client
.genHash(host
);
796 let trackerListItem
= trackerList
.get(hash
);
797 if (trackerListItem
=== undefined) {
798 trackerListItem
= { host
: host
, trackerTorrentMap
: new Map() };
799 trackerList
.set(hash
, trackerListItem
);
802 trackerListItem
.trackerTorrentMap
.set(tracker
, [...torrents
]);
804 updateTrackers
= true;
806 if (response
["trackers_removed"]) {
807 for (let i
= 0; i
< response
["trackers_removed"].length
; ++i
) {
808 const tracker
= response
["trackers_removed"][i
];
809 const hash
= window
.qBittorrent
.Client
.genHash(getHost(tracker
));
810 const trackerListEntry
= trackerList
.get(hash
);
811 if (trackerListEntry
)
812 trackerListEntry
.trackerTorrentMap
.delete(tracker
);
814 updateTrackers
= true;
816 if (response
["torrents"]) {
817 let updateTorrentList
= false;
818 for (const key
in response
["torrents"]) {
819 if (!Object
.hasOwn(response
["torrents"], key
))
822 response
["torrents"][key
]["hash"] = key
;
823 response
["torrents"][key
]["rowId"] = key
;
824 if (response
["torrents"][key
]["state"])
825 response
["torrents"][key
]["status"] = response
["torrents"][key
]["state"];
826 torrentsTable
.updateRowData(response
["torrents"][key
]);
827 if (addTorrentToCategoryList(response
["torrents"][key
]))
828 update_categories
= true;
829 if (addTorrentToTagList(response
["torrents"][key
]))
831 if (response
["torrents"][key
]["name"])
832 updateTorrentList
= true;
835 if (updateTorrentList
)
836 setupCopyEventHandler();
838 if (response
["torrents_removed"]) {
839 response
["torrents_removed"].each((hash
) => {
840 torrentsTable
.removeRow(hash
);
841 removeTorrentFromCategoryList(hash
);
842 update_categories
= true; // Always to update All category
843 removeTorrentFromTagList(hash
);
844 updateTags
= true; // Always to update All tag
847 torrentsTable
.updateTable(full_update
);
848 if (response
["server_state"]) {
849 const tmp
= response
["server_state"];
850 for (const k
in tmp
) {
851 if (!Object
.hasOwn(tmp
, k
))
853 serverState
[k
] = tmp
[k
];
855 processServerState();
858 if (update_categories
) {
859 updateCategoryList();
860 window
.qBittorrent
.TransferList
.contextMenu
.updateCategoriesSubMenu(category_list
);
864 window
.qBittorrent
.TransferList
.contextMenu
.updateTagsSubMenu(tagList
);
870 // re-select previously selected rows
871 torrentsTable
.reselectRows(torrentsTableSelectedRows
);
873 syncRequestInProgress
= false;
874 syncData(window
.qBittorrent
.Client
.getSyncMainDataInterval());
877 syncRequestInProgress
= true;
881 updateMainData = function() {
882 torrentsTable
.updateTable();
886 const syncData = function(delay
) {
887 if (syncRequestInProgress
)
890 clearTimeout(syncMainDataTimeoutID
);
892 if (window
.qBittorrent
.Client
.isStopped())
895 syncMainDataTimeoutID
= syncMainData
.delay(delay
);
898 const processServerState = function() {
899 let transfer_info
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.dl_info_speed
, true);
900 if (serverState
.dl_rate_limit
> 0)
901 transfer_info
+= " [" + window
.qBittorrent
.Misc
.friendlyUnit(serverState
.dl_rate_limit
, true) + "]";
902 transfer_info
+= " (" + window
.qBittorrent
.Misc
.friendlyUnit(serverState
.dl_info_data
, false) + ")";
903 $("DlInfos").textContent
= transfer_info
;
904 transfer_info
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.up_info_speed
, true);
905 if (serverState
.up_rate_limit
> 0)
906 transfer_info
+= " [" + window
.qBittorrent
.Misc
.friendlyUnit(serverState
.up_rate_limit
, true) + "]";
907 transfer_info
+= " (" + window
.qBittorrent
.Misc
.friendlyUnit(serverState
.up_info_data
, false) + ")";
908 $("UpInfos").textContent
= transfer_info
;
910 document
.title
= (speedInTitle
911 ? (`QBT_TR([D: %1, U: %2])QBT_TR[CONTEXT=MainWindow] `
912 .replace("%1", window
.qBittorrent
.Misc
.friendlyUnit(serverState
.dl_info_speed
, true))
913 .replace("%2", window
.qBittorrent
.Misc
.friendlyUnit(serverState
.up_info_speed
, true)))
915 + window
.qBittorrent
.Client
.mainTitle();
917 $("freeSpaceOnDisk").textContent
= "QBT_TR(Free space: %1)QBT_TR[CONTEXT=HttpServer]".replace("%1", window
.qBittorrent
.Misc
.friendlyUnit(serverState
.free_space_on_disk
));
918 $("DHTNodes").textContent
= "QBT_TR(DHT: %1 nodes)QBT_TR[CONTEXT=StatusBar]".replace("%1", serverState
.dht_nodes
);
921 if (document
.getElementById("statisticsContent")) {
922 $("AlltimeDL").textContent
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.alltime_dl
, false);
923 $("AlltimeUL").textContent
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.alltime_ul
, false);
924 $("TotalWastedSession").textContent
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.total_wasted_session
, false);
925 $("GlobalRatio").textContent
= serverState
.global_ratio
;
926 $("TotalPeerConnections").textContent
= serverState
.total_peer_connections
;
927 $("ReadCacheHits").textContent
= serverState
.read_cache_hits
+ "%";
928 $("TotalBuffersSize").textContent
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.total_buffers_size
, false);
929 $("WriteCacheOverload").textContent
= serverState
.write_cache_overload
+ "%";
930 $("ReadCacheOverload").textContent
= serverState
.read_cache_overload
+ "%";
931 $("QueuedIOJobs").textContent
= serverState
.queued_io_jobs
;
932 $("AverageTimeInQueue").textContent
= serverState
.average_time_queue
+ " ms";
933 $("TotalQueuedSize").textContent
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.total_queued_size
, false);
936 switch (serverState
.connection_status
) {
938 $("connectionStatus").src
= "images/connected.svg";
939 $("connectionStatus").alt
= "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
940 $("connectionStatus").title
= "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
943 $("connectionStatus").src
= "images/firewalled.svg";
944 $("connectionStatus").alt
= "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
945 $("connectionStatus").title
= "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
948 $("connectionStatus").src
= "images/disconnected.svg";
949 $("connectionStatus").alt
= "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
950 $("connectionStatus").title
= "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
954 if (queueing_enabled
!== serverState
.queueing
) {
955 queueing_enabled
= serverState
.queueing
;
956 torrentsTable
.columns
["priority"].force_hide
= !queueing_enabled
;
957 torrentsTable
.updateColumn("priority");
958 if (queueing_enabled
) {
959 $("topQueuePosItem").removeClass("invisible");
960 $("increaseQueuePosItem").removeClass("invisible");
961 $("decreaseQueuePosItem").removeClass("invisible");
962 $("bottomQueuePosItem").removeClass("invisible");
963 $("queueingButtons").removeClass("invisible");
964 $("queueingMenuItems").removeClass("invisible");
967 $("topQueuePosItem").addClass("invisible");
968 $("increaseQueuePosItem").addClass("invisible");
969 $("decreaseQueuePosItem").addClass("invisible");
970 $("bottomQueuePosItem").addClass("invisible");
971 $("queueingButtons").addClass("invisible");
972 $("queueingMenuItems").addClass("invisible");
976 if (alternativeSpeedLimits
!== serverState
.use_alt_speed_limits
) {
977 alternativeSpeedLimits
= serverState
.use_alt_speed_limits
;
978 updateAltSpeedIcon(alternativeSpeedLimits
);
981 if (useSubcategories
!== serverState
.use_subcategories
) {
982 useSubcategories
= serverState
.use_subcategories
;
983 updateCategoryList();
986 serverSyncMainDataInterval
= Math
.max(serverState
.refresh_interval
, 500);
989 const updateAltSpeedIcon = function(enabled
) {
991 $("alternativeSpeedLimits").src
= "images/slow.svg";
992 $("alternativeSpeedLimits").alt
= "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
993 $("alternativeSpeedLimits").title
= "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
996 $("alternativeSpeedLimits").src
= "images/slow_off.svg";
997 $("alternativeSpeedLimits").alt
= "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
998 $("alternativeSpeedLimits").title
= "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
1002 $("alternativeSpeedLimits").addEvent("click", () => {
1003 // Change icon immediately to give some feedback
1004 updateAltSpeedIcon(!alternativeSpeedLimits
);
1007 url
: "api/v2/transfer/toggleSpeedLimitsMode",
1009 onComplete: function() {
1010 alternativeSpeedLimits
= !alternativeSpeedLimits
;
1013 onFailure: function() {
1014 // Restore icon in case of failure
1015 updateAltSpeedIcon(alternativeSpeedLimits
);
1020 $("DlInfos").addEvent("click", globalDownloadLimitFN
);
1021 $("UpInfos").addEvent("click", globalUploadLimitFN
);
1023 $("showTopToolbarLink").addEvent("click", (e
) => {
1024 showTopToolbar
= !showTopToolbar
;
1025 LocalPreferences
.set("show_top_toolbar", showTopToolbar
.toString());
1026 if (showTopToolbar
) {
1027 $("showTopToolbarLink").firstChild
.style
.opacity
= "1";
1028 $("mochaToolbar").removeClass("invisible");
1031 $("showTopToolbarLink").firstChild
.style
.opacity
= "0";
1032 $("mochaToolbar").addClass("invisible");
1034 MochaUI
.Desktop
.setDesktopSize();
1037 $("showStatusBarLink").addEvent("click", (e
) => {
1038 showStatusBar
= !showStatusBar
;
1039 LocalPreferences
.set("show_status_bar", showStatusBar
.toString());
1040 if (showStatusBar
) {
1041 $("showStatusBarLink").firstChild
.style
.opacity
= "1";
1042 $("desktopFooterWrapper").removeClass("invisible");
1045 $("showStatusBarLink").firstChild
.style
.opacity
= "0";
1046 $("desktopFooterWrapper").addClass("invisible");
1048 MochaUI
.Desktop
.setDesktopSize();
1051 const registerMagnetHandler = function() {
1052 if (typeof navigator
.registerProtocolHandler
!== "function") {
1053 if (window
.location
.protocol
!== "https:")
1054 alert("QBT_TR(To use this feature, the WebUI needs to be accessed over HTTPS)QBT_TR[CONTEXT=MainWindow]");
1056 alert("QBT_TR(Your browser does not support this feature)QBT_TR[CONTEXT=MainWindow]");
1060 const hashString
= location
.hash
? location
.hash
.replace(/^#/, "") : "";
1061 const hashParams
= new URLSearchParams(hashString
);
1062 hashParams
.set("download", "");
1064 const templateHashString
= hashParams
.toString().replace("download=", "download=%s");
1065 const templateUrl
= location
.origin
+ location
.pathname
1066 + location
.search
+ "#" + templateHashString
;
1068 navigator
.registerProtocolHandler("magnet", templateUrl
,
1069 "qBittorrent WebUI magnet handler");
1071 $("registerMagnetHandlerLink").addEvent("click", (e
) => {
1072 registerMagnetHandler();
1075 $("showFiltersSidebarLink").addEvent("click", (e
) => {
1076 showFiltersSidebar
= !showFiltersSidebar
;
1077 LocalPreferences
.set("show_filters_sidebar", showFiltersSidebar
.toString());
1078 if (showFiltersSidebar
) {
1079 $("showFiltersSidebarLink").firstChild
.style
.opacity
= "1";
1080 $("filtersColumn").removeClass("invisible");
1081 $("filtersColumn_handle").removeClass("invisible");
1084 $("showFiltersSidebarLink").firstChild
.style
.opacity
= "0";
1085 $("filtersColumn").addClass("invisible");
1086 $("filtersColumn_handle").addClass("invisible");
1088 MochaUI
.Desktop
.setDesktopSize();
1091 $("speedInBrowserTitleBarLink").addEvent("click", (e
) => {
1092 speedInTitle
= !speedInTitle
;
1093 LocalPreferences
.set("speed_in_browser_title_bar", speedInTitle
.toString());
1095 $("speedInBrowserTitleBarLink").firstChild
.style
.opacity
= "1";
1097 $("speedInBrowserTitleBarLink").firstChild
.style
.opacity
= "0";
1098 processServerState();
1101 $("showSearchEngineLink").addEvent("click", (e
) => {
1102 window
.qBittorrent
.Client
.showSearchEngine(!window
.qBittorrent
.Client
.isShowSearchEngine());
1103 LocalPreferences
.set("show_search_engine", window
.qBittorrent
.Client
.isShowSearchEngine().toString());
1107 $("showRssReaderLink").addEvent("click", (e
) => {
1108 window
.qBittorrent
.Client
.showRssReader(!window
.qBittorrent
.Client
.isShowRssReader());
1109 LocalPreferences
.set("show_rss_reader", window
.qBittorrent
.Client
.isShowRssReader().toString());
1113 $("showLogViewerLink").addEvent("click", (e
) => {
1114 window
.qBittorrent
.Client
.showLogViewer(!window
.qBittorrent
.Client
.isShowLogViewer());
1115 LocalPreferences
.set("show_log_viewer", window
.qBittorrent
.Client
.isShowLogViewer().toString());
1119 const updateTabDisplay = function() {
1120 if (window
.qBittorrent
.Client
.isShowRssReader()) {
1121 $("showRssReaderLink").firstChild
.style
.opacity
= "1";
1122 $("mainWindowTabs").removeClass("invisible");
1123 $("rssTabLink").removeClass("invisible");
1124 if (!MochaUI
.Panels
.instances
.RssPanel
)
1128 $("showRssReaderLink").firstChild
.style
.opacity
= "0";
1129 $("rssTabLink").addClass("invisible");
1130 if ($("rssTabLink").hasClass("selected"))
1131 $("transfersTabLink").click();
1134 if (window
.qBittorrent
.Client
.isShowSearchEngine()) {
1135 $("showSearchEngineLink").firstChild
.style
.opacity
= "1";
1136 $("mainWindowTabs").removeClass("invisible");
1137 $("searchTabLink").removeClass("invisible");
1138 if (!MochaUI
.Panels
.instances
.SearchPanel
)
1142 $("showSearchEngineLink").firstChild
.style
.opacity
= "0";
1143 $("searchTabLink").addClass("invisible");
1144 if ($("searchTabLink").hasClass("selected"))
1145 $("transfersTabLink").click();
1148 if (window
.qBittorrent
.Client
.isShowLogViewer()) {
1149 $("showLogViewerLink").firstChild
.style
.opacity
= "1";
1150 $("mainWindowTabs").removeClass("invisible");
1151 $("logTabLink").removeClass("invisible");
1152 if (!MochaUI
.Panels
.instances
.LogPanel
)
1156 $("showLogViewerLink").firstChild
.style
.opacity
= "0";
1157 $("logTabLink").addClass("invisible");
1158 if ($("logTabLink").hasClass("selected"))
1159 $("transfersTabLink").click();
1163 if (!window
.qBittorrent
.Client
.isShowRssReader() && !window
.qBittorrent
.Client
.isShowSearchEngine() && !window
.qBittorrent
.Client
.isShowLogViewer())
1164 $("mainWindowTabs").addClass("invisible");
1167 $("StatisticsLink").addEvent("click", StatisticsLinkFN
);
1171 const showTransfersTab = function() {
1172 const showFiltersSidebar
= LocalPreferences
.get("show_filters_sidebar", "true") === "true";
1173 if (showFiltersSidebar
) {
1174 $("filtersColumn").removeClass("invisible");
1175 $("filtersColumn_handle").removeClass("invisible");
1177 $("mainColumn").removeClass("invisible");
1178 $("torrentsFilterToolbar").removeClass("invisible");
1180 customSyncMainDataInterval
= null;
1187 LocalPreferences
.set("selected_window_tab", "transfers");
1190 const hideTransfersTab = function() {
1191 $("filtersColumn").addClass("invisible");
1192 $("filtersColumn_handle").addClass("invisible");
1193 $("mainColumn").addClass("invisible");
1194 $("torrentsFilterToolbar").addClass("invisible");
1195 MochaUI
.Desktop
.resizePanels();
1198 const showSearchTab
= (function() {
1199 let searchTabInitialized
= false;
1202 // we must wait until the panel is fully loaded before proceeding.
1203 // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1204 // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1205 if (!isSearchPanelLoaded
) {
1212 if (!searchTabInitialized
) {
1213 window
.qBittorrent
.Search
.init();
1214 searchTabInitialized
= true;
1217 $("searchTabColumn").removeClass("invisible");
1218 customSyncMainDataInterval
= 30000;
1223 LocalPreferences
.set("selected_window_tab", "search");
1227 const hideSearchTab = function() {
1228 $("searchTabColumn").addClass("invisible");
1229 MochaUI
.Desktop
.resizePanels();
1232 const showRssTab
= (function() {
1233 let rssTabInitialized
= false;
1236 if (!rssTabInitialized
) {
1237 window
.qBittorrent
.Rss
.init();
1238 rssTabInitialized
= true;
1241 window
.qBittorrent
.Rss
.load();
1244 $("rssTabColumn").removeClass("invisible");
1245 customSyncMainDataInterval
= 30000;
1250 LocalPreferences
.set("selected_window_tab", "rss");
1254 const hideRssTab = function() {
1255 $("rssTabColumn").addClass("invisible");
1256 window
.qBittorrent
.Rss
&& window
.qBittorrent
.Rss
.unload();
1257 MochaUI
.Desktop
.resizePanels();
1260 const showLogTab
= (function() {
1261 let logTabInitialized
= false;
1264 // we must wait until the panel is fully loaded before proceeding.
1265 // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1266 // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1267 if (!isLogPanelLoaded
) {
1274 if (!logTabInitialized
) {
1275 window
.qBittorrent
.Log
.init();
1276 logTabInitialized
= true;
1279 window
.qBittorrent
.Log
.load();
1282 $("logTabColumn").removeClass("invisible");
1283 customSyncMainDataInterval
= 30000;
1288 LocalPreferences
.set("selected_window_tab", "log");
1292 const hideLogTab = function() {
1293 $("logTabColumn").addClass("invisible");
1294 MochaUI
.Desktop
.resizePanels();
1295 window
.qBittorrent
.Log
&& window
.qBittorrent
.Log
.unload();
1298 const addSearchPanel = function() {
1310 contentURL
: "views/search.html",
1312 js
: ["scripts/search.js"],
1314 isSearchPanelLoaded
= true;
1318 column
: "searchTabColumn",
1323 const addRssPanel = function() {
1335 contentURL
: "views/rss.html",
1337 column
: "rssTabColumn",
1342 const addLogPanel = function() {
1354 contentURL
: "views/log.html",
1356 css
: ["css/vanillaSelectBox.css"],
1357 js
: ["scripts/lib/vanillaSelectBox.js"],
1359 isLogPanelLoaded
= true;
1362 tabsURL
: "views/logTabs.html",
1363 tabsOnload: function() {
1364 MochaUI
.initializeTabs("panelTabs");
1366 $("logMessageLink").addEvent("click", (e
) => {
1367 window
.qBittorrent
.Log
.setCurrentTab("main");
1370 $("logPeerLink").addEvent("click", (e
) => {
1371 window
.qBittorrent
.Log
.setCurrentTab("peer");
1376 column
: "logTabColumn",
1381 const handleDownloadParam = function() {
1382 // Extract torrent URL from download param in WebUI URL hash
1383 const downloadHash
= "#download=";
1384 if (location
.hash
.indexOf(downloadHash
) !== 0)
1387 const url
= decodeURIComponent(location
.hash
.substring(downloadHash
.length
));
1388 // Remove the processed hash from the URL
1389 history
.replaceState("", document
.title
, (location
.pathname
+ location
.search
));
1390 showDownloadPage([url
]);
1404 contentURL
: "views/transferlist.html",
1405 onContentLoaded: function() {
1406 handleDownloadParam();
1409 column
: "mainColumn",
1410 onResize
: saveColumnSizes
,
1413 let prop_h
= LocalPreferences
.get("properties_height_rel");
1414 if (prop_h
!== null)
1415 prop_h
= prop_h
.toFloat() * Window
.getSize().y
;
1417 prop_h
= Window
.getSize().y
/ 2.0;
1419 id
: "propertiesPanel",
1428 contentURL
: "views/properties.html",
1430 css
: ["css/Tabs.css", "css/dynamicTable.css"],
1431 js
: ["scripts/prop-general.js", "scripts/prop-trackers.js", "scripts/prop-peers.js", "scripts/prop-webseeds.js", "scripts/prop-files.js"],
1433 tabsURL
: "views/propertiesToolbar.html",
1434 tabsOnload: function() {
1435 MochaUI
.initializeTabs("propertiesTabs");
1437 updatePropertiesPanel = function() {
1438 if (!$("prop_general").hasClass("invisible")) {
1439 if (window
.qBittorrent
.PropGeneral
!== undefined)
1440 window
.qBittorrent
.PropGeneral
.updateData();
1442 else if (!$("prop_trackers").hasClass("invisible")) {
1443 if (window
.qBittorrent
.PropTrackers
!== undefined)
1444 window
.qBittorrent
.PropTrackers
.updateData();
1446 else if (!$("prop_peers").hasClass("invisible")) {
1447 if (window
.qBittorrent
.PropPeers
!== undefined)
1448 window
.qBittorrent
.PropPeers
.updateData();
1450 else if (!$("prop_webseeds").hasClass("invisible")) {
1451 if (window
.qBittorrent
.PropWebseeds
!== undefined)
1452 window
.qBittorrent
.PropWebseeds
.updateData();
1454 else if (!$("prop_files").hasClass("invisible")) {
1455 if (window
.qBittorrent
.PropFiles
!== undefined)
1456 window
.qBittorrent
.PropFiles
.updateData();
1460 $("PropGeneralLink").addEvent("click", function(e
) {
1461 $$(".propertiesTabContent").addClass("invisible");
1462 $("prop_general").removeClass("invisible");
1464 updatePropertiesPanel();
1465 LocalPreferences
.set("selected_tab", this.id
);
1468 $("PropTrackersLink").addEvent("click", function(e
) {
1469 $$(".propertiesTabContent").addClass("invisible");
1470 $("prop_trackers").removeClass("invisible");
1472 updatePropertiesPanel();
1473 LocalPreferences
.set("selected_tab", this.id
);
1476 $("PropPeersLink").addEvent("click", function(e
) {
1477 $$(".propertiesTabContent").addClass("invisible");
1478 $("prop_peers").removeClass("invisible");
1480 updatePropertiesPanel();
1481 LocalPreferences
.set("selected_tab", this.id
);
1484 $("PropWebSeedsLink").addEvent("click", function(e
) {
1485 $$(".propertiesTabContent").addClass("invisible");
1486 $("prop_webseeds").removeClass("invisible");
1488 updatePropertiesPanel();
1489 LocalPreferences
.set("selected_tab", this.id
);
1492 $("PropFilesLink").addEvent("click", function(e
) {
1493 $$(".propertiesTabContent").addClass("invisible");
1494 $("prop_files").removeClass("invisible");
1496 updatePropertiesPanel();
1497 LocalPreferences
.set("selected_tab", this.id
);
1500 $("propertiesPanel_collapseToggle").addEvent("click", (e
) => {
1501 updatePropertiesPanel();
1504 column
: "mainColumn",
1508 const showFilesFilter = function() {
1509 $("torrentFilesFilterToolbar").removeClass("invisible");
1512 const hideFilesFilter = function() {
1513 $("torrentFilesFilterToolbar").addClass("invisible");
1516 // listen for changes to torrentsFilterInput
1517 let torrentsFilterInputTimer
= -1;
1518 $("torrentsFilterInput").addEvent("input", () => {
1519 clearTimeout(torrentsFilterInputTimer
);
1520 torrentsFilterInputTimer
= setTimeout(() => {
1521 torrentsFilterInputTimer
= -1;
1522 torrentsTable
.updateTable();
1523 }, window
.qBittorrent
.Misc
.FILTER_INPUT_DELAY
);
1525 $("torrentsFilterRegexBox").addEvent("change", () => {
1526 torrentsTable
.updateTable();
1529 $("transfersTabLink").addEvent("click", showTransfersTab
);
1530 $("searchTabLink").addEvent("click", showSearchTab
);
1531 $("rssTabLink").addEvent("click", showRssTab
);
1532 $("logTabLink").addEvent("click", showLogTab
);
1535 const registerDragAndDrop
= () => {
1536 $("desktop").addEventListener("dragover", (ev
) => {
1537 if (ev
.preventDefault
)
1538 ev
.preventDefault();
1541 $("desktop").addEventListener("dragenter", (ev
) => {
1542 if (ev
.preventDefault
)
1543 ev
.preventDefault();
1546 $("desktop").addEventListener("drop", (ev
) => {
1547 if (ev
.preventDefault
)
1548 ev
.preventDefault();
1550 const droppedFiles
= ev
.dataTransfer
.files
;
1552 if (droppedFiles
.length
> 0) {
1553 // dropped files or folders
1555 // can't handle folder due to cannot put the filelist (from dropped folder)
1556 // to <input> `files` field
1557 for (const item
of ev
.dataTransfer
.items
) {
1558 if (item
.webkitGetAsEntry().isDirectory
)
1562 const id
= "uploadPage";
1563 new MochaUI
.Window({
1565 title
: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
1566 loadMethod
: "iframe",
1567 contentURL
: new URI("upload.html").toString(),
1568 addClass
: "windowFrame", // fixes iframe scrolling on iOS Safari
1572 paddingHorizontal
: 0,
1573 width
: loadWindowWidth(id
, 500),
1574 height
: loadWindowHeight(id
, 460),
1578 onContentLoaded
: () => {
1579 const fileInput
= $(`${id}_iframe`).contentDocument
.getElementById("fileselect");
1580 fileInput
.files
= droppedFiles
;
1585 const droppedText
= ev
.dataTransfer
.getData("text");
1586 if (droppedText
.length
> 0) {
1589 const urls
= droppedText
.split("\n")
1590 .map((str
) => str
.trim())
1592 const lowercaseStr
= str
.toLowerCase();
1593 return lowercaseStr
.startsWith("http:")
1594 || lowercaseStr
.startsWith("https:")
1595 || lowercaseStr
.startsWith("magnet:")
1596 || ((str
.length
=== 40) && !(/[^0-9A-F]/i.test(str
))) // v1 hex-encoded SHA-1 info-hash
1597 || ((str
.length
=== 32) && !(/[^2-7A-Z]/i.test(str
))); // v1 Base32 encoded SHA-1 info-hash
1600 if (urls
.length
<= 0)
1603 const id
= "downloadPage";
1604 const contentURI
= new URI("download.html").setData("urls", urls
.map(encodeURIComponent
).join("|"));
1605 new MochaUI
.Window({
1607 title
: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
1608 loadMethod
: "iframe",
1609 contentURL
: contentURI
.toString(),
1610 addClass
: "windowFrame", // fixes iframe scrolling on iOS Safari
1615 paddingHorizontal
: 0,
1616 width
: loadWindowWidth(id
, 500),
1617 height
: loadWindowHeight(id
, 600),
1625 registerDragAndDrop();
1628 defaultEventType
: "keydown",
1630 "ctrl+a": function(event
) {
1631 if ((event
.target
.nodeName
=== "INPUT") || (event
.target
.nodeName
=== "TEXTAREA"))
1633 if (event
.target
.isContentEditable
)
1635 torrentsTable
.selectAll();
1636 event
.preventDefault();
1638 "delete": function(event
) {
1639 if ((event
.target
.nodeName
=== "INPUT") || (event
.target
.nodeName
=== "TEXTAREA"))
1641 if (event
.target
.isContentEditable
)
1644 event
.preventDefault();
1646 "shift+delete": (event
) => {
1647 if ((event
.target
.nodeName
=== "INPUT") || (event
.target
.nodeName
=== "TEXTAREA"))
1649 if (event
.target
.isContentEditable
)
1652 event
.preventDefault();
1658 window
.addEventListener("load", () => {
1659 // fetch various data and store it in memory
1660 window
.qBittorrent
.Cache
.buildInfo
.init();
1661 window
.qBittorrent
.Cache
.preferences
.init();
1662 window
.qBittorrent
.Cache
.qbtVersion
.init();
1664 // switch to previously used tab
1665 const previouslyUsedTab
= LocalPreferences
.get("selected_window_tab", "transfers");
1666 switch (previouslyUsedTab
) {
1668 if (window
.qBittorrent
.Client
.isShowSearchEngine())
1669 $("searchTabLink").click();
1672 if (window
.qBittorrent
.Client
.isShowRssReader())
1673 $("rssTabLink").click();
1676 if (window
.qBittorrent
.Client
.isShowLogViewer())
1677 $("logTabLink").click();
1680 $("transfersTabLink").click();
1683 console
.error(`Unexpected 'selected_window_tab' value: ${previouslyUsedTab}`);
1684 $("transfersTabLink").click();