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 closeWindow
: closeWindow
,
33 closeWindows
: closeWindows
,
35 getSyncMainDataInterval
: getSyncMainDataInterval
,
39 showSearchEngine
: showSearchEngine
,
40 showRssReader
: showRssReader
,
41 showLogViewer
: showLogViewer
,
42 isShowSearchEngine
: isShowSearchEngine
,
43 isShowRssReader
: isShowRssReader
,
44 isShowLogViewer
: isShowLogViewer
48 const closeWindow = function(windowID
) {
49 const window
= document
.getElementById(windowID
);
52 MochaUI
.closeWindow(window
);
55 const closeWindows = function() {
59 const genHash = function(string
) {
61 // https://stackoverflow.com/a/8831937
62 // https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0
64 for (let i
= 0; i
< string
.length
; ++i
)
65 hash
= ((Math
.imul(hash
, 31) + string
.charCodeAt(i
)) | 0);
69 const getSyncMainDataInterval = function() {
70 return customSyncMainDataInterval
? customSyncMainDataInterval
: serverSyncMainDataInterval
;
74 const isStopped
= () => {
82 const mainTitle
= () => {
83 const emDash
= "\u2014";
84 const qbtVersion
= window
.qBittorrent
.Cache
.qbtVersion
.get();
85 const suffix
= window
.qBittorrent
.Cache
.preferences
.get()["app_instance_name"] || "";
86 const title
= `qBittorrent ${qbtVersion} QBT_TR(WebUI)QBT_TR[CONTEXT=OptionsDialog]`
87 + ((suffix
.length
> 0) ? ` ${emDash} ${suffix}` : "");
91 let showingSearchEngine
= false;
92 let showingRssReader
= false;
93 let showingLogViewer
= false;
95 const showSearchEngine = function(bool
) {
96 showingSearchEngine
= bool
;
98 const showRssReader = function(bool
) {
99 showingRssReader
= bool
;
101 const showLogViewer = function(bool
) {
102 showingLogViewer
= bool
;
104 const isShowSearchEngine = function() {
105 return showingSearchEngine
;
107 const isShowRssReader = function() {
108 return showingRssReader
;
110 const isShowLogViewer = function() {
111 return showingLogViewer
;
116 Object
.freeze(window
.qBittorrent
.Client
);
118 // TODO: move global functions/variables into some namespace/scope
120 this.torrentsTable
= new window
.qBittorrent
.DynamicTable
.TorrentsTable();
122 let updatePropertiesPanel = function() {};
124 this.updateMainData = function() {};
125 let alternativeSpeedLimits
= false;
126 let queueing_enabled
= true;
127 let serverSyncMainDataInterval
= 1500;
128 let customSyncMainDataInterval
= null;
129 let useSubcategories
= true;
130 const useAutoHideZeroStatusFilters
= LocalPreferences
.get("hide_zero_status_filters", "false") === "true";
132 /* Categories filter */
133 const CATEGORIES_ALL
= 1;
134 const CATEGORIES_UNCATEGORIZED
= 2;
136 const category_list
= new Map();
138 let selectedCategory
= Number(LocalPreferences
.get("selected_category", CATEGORIES_ALL
));
139 let setCategoryFilter = function() {};
143 const TAGS_UNTAGGED
= 2;
145 const tagList
= new Map();
147 let selectedTag
= Number(LocalPreferences
.get("selected_tag", TAGS_ALL
));
148 let setTagFilter = function() {};
150 /* Trackers filter */
151 const TRACKERS_ALL
= 1;
152 const TRACKERS_TRACKERLESS
= 2;
154 /** @type Map<number, {host: string, trackerTorrentMap: Map<string, string[]>}> **/
155 const trackerList
= new Map();
157 let selectedTracker
= Number(LocalPreferences
.get("selected_tracker", TRACKERS_ALL
));
158 let setTrackerFilter = function() {};
161 let selectedStatus
= LocalPreferences
.get("selected_filter", "all");
162 let setStatusFilter = function() {};
163 let toggleFilterDisplay = function() {};
165 window
.addEventListener("DOMContentLoaded", () => {
166 let isSearchPanelLoaded
= false;
167 let isLogPanelLoaded
= false;
169 const saveColumnSizes = function() {
170 const filters_width
= $("Filters").getSize().x
;
171 LocalPreferences
.set("filters_width", filters_width
);
172 const properties_height_rel
= $("propertiesPanel").getSize().y
/ Window
.getSize().y
;
173 LocalPreferences
.set("properties_height_rel", properties_height_rel
);
176 window
.addEventListener("resize", window
.qBittorrent
.Misc
.createDebounceHandler(500, (e
) => {
177 // only save sizes if the columns are visible
178 if (!$("mainColumn").hasClass("invisible"))
182 /* MochaUI.Desktop = new MochaUI.Desktop();
183 MochaUI.Desktop.desktop.style.background = "#fff";
184 MochaUI.Desktop.desktop.style.visibility = "visible"; */
185 MochaUI
.Desktop
.initialize();
187 const buildTransfersTab = function() {
188 const filt_w
= Number(LocalPreferences
.get("filters_width", 120));
192 onResize
: window
.qBittorrent
.Misc
.createDebounceHandler(500, (e
) => {
196 resizeLimit
: [1, 300]
204 const buildSearchTab = function() {
206 id
: "searchTabColumn",
212 $("searchTabColumn").addClass("invisible");
215 const buildRssTab = function() {
223 $("rssTabColumn").addClass("invisible");
226 const buildLogTab = function() {
234 $("logTabColumn").addClass("invisible");
241 MochaUI
.initializeTabs("mainWindowTabsList");
243 setStatusFilter = function(name
) {
244 LocalPreferences
.set("selected_filter", name
);
245 selectedStatus
= name
;
246 highlightSelectedStatus();
250 setCategoryFilter = function(hash
) {
251 LocalPreferences
.set("selected_category", hash
);
252 selectedCategory
= Number(hash
);
253 highlightSelectedCategory();
257 setTagFilter = function(hash
) {
258 LocalPreferences
.set("selected_tag", hash
);
259 selectedTag
= Number(hash
);
260 highlightSelectedTag();
264 setTrackerFilter = function(hash
) {
265 LocalPreferences
.set("selected_tracker", hash
);
266 selectedTracker
= Number(hash
);
267 highlightSelectedTracker();
271 toggleFilterDisplay = function(filterListID
) {
272 const filterList
= document
.getElementById(filterListID
);
273 const filterTitle
= filterList
.previousElementSibling
;
274 const toggleIcon
= filterTitle
.firstElementChild
;
275 toggleIcon
.classList
.toggle("rotate");
276 LocalPreferences
.set(`filter_${filterListID.replace("FilterList", "")}_collapsed`, filterList
.classList
.toggle("invisible").toString());
290 contentURL
: "views/filters.html",
291 onContentLoaded: function() {
292 highlightSelectedStatus();
294 column
: "filtersColumn",
299 // Show Top Toolbar is enabled by default
300 let showTopToolbar
= LocalPreferences
.get("show_top_toolbar", "true") === "true";
301 if (!showTopToolbar
) {
302 $("showTopToolbarLink").firstChild
.style
.opacity
= "0";
303 $("mochaToolbar").addClass("invisible");
306 // Show Status Bar is enabled by default
307 let showStatusBar
= LocalPreferences
.get("show_status_bar", "true") === "true";
308 if (!showStatusBar
) {
309 $("showStatusBarLink").firstChild
.style
.opacity
= "0";
310 $("desktopFooterWrapper").addClass("invisible");
313 // Show Filters Sidebar is enabled by default
314 let showFiltersSidebar
= LocalPreferences
.get("show_filters_sidebar", "true") === "true";
315 if (!showFiltersSidebar
) {
316 $("showFiltersSidebarLink").firstChild
.style
.opacity
= "0";
317 $("filtersColumn").addClass("invisible");
318 $("filtersColumn_handle").addClass("invisible");
321 let speedInTitle
= LocalPreferences
.get("speed_in_browser_title_bar") === "true";
323 $("speedInBrowserTitleBarLink").firstChild
.style
.opacity
= "0";
325 // After showing/hiding the toolbar + status bar
326 window
.qBittorrent
.Client
.showSearchEngine(LocalPreferences
.get("show_search_engine") !== "false");
327 window
.qBittorrent
.Client
.showRssReader(LocalPreferences
.get("show_rss_reader") !== "false");
328 window
.qBittorrent
.Client
.showLogViewer(LocalPreferences
.get("show_log_viewer") === "true");
330 // After Show Top Toolbar
331 MochaUI
.Desktop
.setDesktopSize();
333 let syncMainDataLastResponseId
= 0;
334 const serverState
= {};
336 const removeTorrentFromCategoryList = function(hash
) {
341 category_list
.forEach((category
) => {
342 const deleteResult
= category
.torrents
.delete(hash
);
343 removed
||= deleteResult
;
349 const addTorrentToCategoryList = function(torrent
) {
350 const category
= torrent
["category"];
351 if (typeof category
=== "undefined")
354 const hash
= torrent
["hash"];
355 if (category
.length
=== 0) { // Empty category
356 removeTorrentFromCategoryList(hash
);
360 const categoryHash
= window
.qBittorrent
.Client
.genHash(category
);
361 if (!category_list
.has(categoryHash
)) { // This should not happen
362 category_list
.set(categoryHash
, {
368 const torrents
= category_list
.get(categoryHash
).torrents
;
369 if (!torrents
.has(hash
)) {
370 removeTorrentFromCategoryList(hash
);
377 const removeTorrentFromTagList = function(hash
) {
382 tagList
.forEach((tag
) => {
383 const deleteResult
= tag
.torrents
.delete(hash
);
384 removed
||= deleteResult
;
390 const addTorrentToTagList = function(torrent
) {
391 if (torrent
["tags"] === undefined) // Tags haven't changed
394 const hash
= torrent
["hash"];
395 removeTorrentFromTagList(hash
);
397 if (torrent
["tags"].length
=== 0) // No tags
400 const tags
= torrent
["tags"].split(",");
402 for (let i
= 0; i
< tags
.length
; ++i
) {
403 const tagHash
= window
.qBittorrent
.Client
.genHash(tags
[i
].trim());
404 if (!tagList
.has(tagHash
)) { // This should not happen
405 tagList
.set(tagHash
, {
411 const torrents
= tagList
.get(tagHash
).torrents
;
412 if (!torrents
.has(hash
)) {
420 const updateFilter = function(filter
, filterTitle
) {
421 const filterEl
= document
.getElementById(`${filter}_filter`);
422 const filterTorrentCount
= torrentsTable
.getFilteredTorrentsNumber(filter
, CATEGORIES_ALL
, TAGS_ALL
, TRACKERS_ALL
);
423 if (useAutoHideZeroStatusFilters
) {
424 const hideFilter
= (filterTorrentCount
=== 0) && (filter
!== "all");
425 if (filterEl
.classList
.toggle("invisible", hideFilter
))
428 filterEl
.firstElementChild
.lastChild
.nodeValue
= filterTitle
.replace("%1", filterTorrentCount
);
431 const updateFiltersList = function() {
432 updateFilter("all", "QBT_TR(All (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
433 updateFilter("downloading", "QBT_TR(Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
434 updateFilter("seeding", "QBT_TR(Seeding (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
435 updateFilter("completed", "QBT_TR(Completed (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
436 updateFilter("running", "QBT_TR(Running (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
437 updateFilter("stopped", "QBT_TR(Stopped (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
438 updateFilter("active", "QBT_TR(Active (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
439 updateFilter("inactive", "QBT_TR(Inactive (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
440 updateFilter("stalled", "QBT_TR(Stalled (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
441 updateFilter("stalled_uploading", "QBT_TR(Stalled Uploading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
442 updateFilter("stalled_downloading", "QBT_TR(Stalled Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
443 updateFilter("checking", "QBT_TR(Checking (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
444 updateFilter("moving", "QBT_TR(Moving (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
445 updateFilter("errored", "QBT_TR(Errored (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
448 const highlightSelectedStatus = function() {
449 const statusFilter
= document
.getElementById("statusFilterList");
450 const filterID
= `${selectedStatus}_filter`;
451 for (const status
of statusFilter
.children
)
452 status
.classList
.toggle("selectedFilter", (status
.id
=== filterID
));
455 const updateCategoryList = function() {
456 const categoryList
= $("categoryFilterList");
459 categoryList
.getChildren().each(c
=> c
.destroy());
461 const categoryItemTemplate
= document
.getElementById("categoryFilterItem");
463 const create_link = function(hash
, text
, count
) {
464 let display_name
= text
;
466 if (useSubcategories
) {
467 const category_path
= text
.split("/");
468 display_name
= category_path
[category_path
.length
- 1];
469 margin_left
= (category_path
.length
- 1) * 20;
472 const categoryFilterItem
= categoryItemTemplate
.content
.cloneNode(true).firstElementChild
;
473 categoryFilterItem
.id
= hash
;
474 categoryFilterItem
.classList
.toggle("selectedFilter", hash
=== selectedCategory
);
476 const span
= categoryFilterItem
.firstElementChild
;
477 span
.style
.marginLeft
= `${margin_left}px`;
478 span
.lastChild
.textContent
= `${display_name} (${count})`;
480 return categoryFilterItem
;
483 const all
= torrentsTable
.getRowIds().length
;
484 let uncategorized
= 0;
485 for (const key
in torrentsTable
.rows
) {
486 if (!Object
.hasOwn(torrentsTable
.rows
, key
))
489 const row
= torrentsTable
.rows
[key
];
490 if (row
["full_data"].category
.length
=== 0)
493 categoryList
.appendChild(create_link(CATEGORIES_ALL
, "QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]", all
));
494 categoryList
.appendChild(create_link(CATEGORIES_UNCATEGORIZED
, "QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]", uncategorized
));
496 const sortedCategories
= [];
497 category_list
.forEach((category
, hash
) => sortedCategories
.push({
498 categoryName
: category
.name
,
500 categoryCount
: category
.torrents
.size
502 sortedCategories
.sort((left
, right
) => {
503 const leftSegments
= left
.categoryName
.split("/");
504 const rightSegments
= right
.categoryName
.split("/");
506 for (let i
= 0, iMax
= Math
.min(leftSegments
.length
, rightSegments
.length
); i
< iMax
; ++i
) {
507 const compareResult
= window
.qBittorrent
.Misc
.naturalSortCollator
.compare(
508 leftSegments
[i
], rightSegments
[i
]);
509 if (compareResult
!== 0)
510 return compareResult
;
513 return leftSegments
.length
- rightSegments
.length
;
516 for (let i
= 0; i
< sortedCategories
.length
; ++i
) {
517 const { categoryName
, categoryHash
} = sortedCategories
[i
];
518 let { categoryCount
} = sortedCategories
[i
];
520 if (useSubcategories
) {
521 for (let j
= (i
+ 1);
522 ((j
< sortedCategories
.length
) && sortedCategories
[j
].categoryName
.startsWith(categoryName
+ "/")); ++j
)
523 categoryCount
+= sortedCategories
[j
].categoryCount
;
526 categoryList
.appendChild(create_link(categoryHash
, categoryName
, categoryCount
));
529 window
.qBittorrent
.Filters
.categoriesFilterContextMenu
.searchAndAddTargets();
532 const highlightSelectedCategory = function() {
533 const categoryList
= document
.getElementById("categoryFilterList");
537 for (const category
of categoryList
.children
)
538 category
.classList
.toggle("selectedFilter", (Number(category
.id
) === selectedCategory
));
541 const updateTagList = function() {
542 const tagFilterList
= $("tagFilterList");
543 if (tagFilterList
=== null)
546 tagFilterList
.getChildren().each(c
=> c
.destroy());
548 const tagItemTemplate
= document
.getElementById("tagFilterItem");
550 const createLink = function(hash
, text
, count
) {
551 const tagFilterItem
= tagItemTemplate
.content
.cloneNode(true).firstElementChild
;
552 tagFilterItem
.id
= hash
;
553 tagFilterItem
.classList
.toggle("selectedFilter", hash
=== selectedTag
);
555 const span
= tagFilterItem
.firstElementChild
;
556 span
.lastChild
.textContent
= `${text} (${count})`;
558 return tagFilterItem
;
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 window
.qBittorrent
.Filters
.tagsFilterContextMenu
.searchAndAddTargets();
584 const highlightSelectedTag = function() {
585 const tagFilterList
= document
.getElementById("tagFilterList");
589 for (const tag
of tagFilterList
.children
)
590 tag
.classList
.toggle("selectedFilter", (Number(tag
.id
) === selectedTag
));
593 // getHost emulate the GUI version `QString getHost(const QString &url)`
594 const getHost = function(url
) {
595 // We want the hostname.
596 // If failed to parse the domain, original input should be returned
598 if (!/^(?:https?|udp):/i.test(url
))
602 // hack: URL can not get hostname from udp protocol
603 const parsedUrl
= new URL(url
.replace(/^udp:/i, "https:"));
604 // host: "example.com:8443"
605 // hostname: "example.com"
606 const host
= parsedUrl
.hostname
;
617 const updateTrackerList = function() {
618 const trackerFilterList
= $("trackerFilterList");
619 if (trackerFilterList
=== null)
622 trackerFilterList
.getChildren().each(c
=> c
.destroy());
624 const trackerItemTemplate
= document
.getElementById("trackerFilterItem");
626 const createLink = function(hash
, text
, count
) {
627 const trackerFilterItem
= trackerItemTemplate
.content
.cloneNode(true).firstElementChild
;
628 trackerFilterItem
.id
= hash
;
629 trackerFilterItem
.classList
.toggle("selectedFilter", hash
=== selectedTracker
);
631 const span
= trackerFilterItem
.firstElementChild
;
632 span
.lastChild
.textContent
= text
.replace("%1", count
);
634 return trackerFilterItem
;
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 window
.qBittorrent
.Filters
.trackersFilterContextMenu
.searchAndAddTargets();
668 const highlightSelectedTracker = function() {
669 const trackerFilterList
= document
.getElementById("trackerFilterList");
670 if (!trackerFilterList
)
673 for (const tracker
of trackerFilterList
.children
)
674 tracker
.classList
.toggle("selectedFilter", (Number(tracker
.id
) === selectedTracker
));
677 const setupCopyEventHandler
= (function() {
682 clipboardEvent
.destroy();
684 clipboardEvent
= new ClipboardJS(".copyToClipboard", {
685 text: function(trigger
) {
686 switch (trigger
.id
) {
689 case "copyInfohash1":
690 return copyInfohashFN(1);
691 case "copyInfohash2":
692 return copyInfohashFN(2);
693 case "copyMagnetLink":
694 return copyMagnetLinkFN();
698 return copyCommentFN();
707 let syncMainDataTimeoutID
= -1;
708 let syncRequestInProgress
= false;
709 const syncMainData = function() {
710 const url
= new URI("api/v2/sync/maindata");
711 url
.setData("rid", syncMainDataLastResponseId
);
712 const request
= new Request
.JSON({
716 onFailure: function() {
717 const errorDiv
= $("error_div");
719 errorDiv
.textContent
= "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]";
720 syncRequestInProgress
= false;
723 onSuccess: function(response
) {
724 $("error_div").textContent
= "";
726 clearTimeout(torrentsFilterInputTimer
);
727 torrentsFilterInputTimer
= -1;
729 let torrentsTableSelectedRows
;
730 let update_categories
= false;
731 let updateTags
= false;
732 let updateTrackers
= false;
733 const full_update
= (response
["full_update"] === true);
735 torrentsTableSelectedRows
= torrentsTable
.selectedRowsIds();
736 update_categories
= true;
738 updateTrackers
= true;
739 torrentsTable
.clear();
740 category_list
.clear();
745 syncMainDataLastResponseId
= response
["rid"];
746 if (response
["categories"]) {
747 for (const key
in response
["categories"]) {
748 if (!Object
.hasOwn(response
["categories"], key
))
751 const responseCategory
= response
["categories"][key
];
752 const categoryHash
= window
.qBittorrent
.Client
.genHash(key
);
753 const category
= category_list
.get(categoryHash
);
754 if (category
!== undefined) {
755 // only the save path can change for existing categories
756 category
.savePath
= responseCategory
.savePath
;
759 category_list
.set(categoryHash
, {
760 name
: responseCategory
.name
,
761 savePath
: responseCategory
.savePath
,
766 update_categories
= true;
768 if (response
["categories_removed"]) {
769 response
["categories_removed"].each((category
) => {
770 const categoryHash
= window
.qBittorrent
.Client
.genHash(category
);
771 category_list
.delete(categoryHash
);
773 update_categories
= true;
775 if (response
["tags"]) {
776 for (const tag
of response
["tags"]) {
777 const tagHash
= window
.qBittorrent
.Client
.genHash(tag
);
778 if (!tagList
.has(tagHash
)) {
779 tagList
.set(tagHash
, {
787 if (response
["tags_removed"]) {
788 for (let i
= 0; i
< response
["tags_removed"].length
; ++i
) {
789 const tagHash
= window
.qBittorrent
.Client
.genHash(response
["tags_removed"][i
]);
790 tagList
.delete(tagHash
);
794 if (response
["trackers"]) {
795 for (const [tracker
, torrents
] of Object
.entries(response
["trackers"])) {
796 const host
= getHost(tracker
);
797 const hash
= window
.qBittorrent
.Client
.genHash(host
);
799 let trackerListItem
= trackerList
.get(hash
);
800 if (trackerListItem
=== undefined) {
801 trackerListItem
= { host
: host
, trackerTorrentMap
: new Map() };
802 trackerList
.set(hash
, trackerListItem
);
805 trackerListItem
.trackerTorrentMap
.set(tracker
, [...torrents
]);
807 updateTrackers
= true;
809 if (response
["trackers_removed"]) {
810 for (let i
= 0; i
< response
["trackers_removed"].length
; ++i
) {
811 const tracker
= response
["trackers_removed"][i
];
812 const hash
= window
.qBittorrent
.Client
.genHash(getHost(tracker
));
813 const trackerListEntry
= trackerList
.get(hash
);
814 if (trackerListEntry
)
815 trackerListEntry
.trackerTorrentMap
.delete(tracker
);
817 updateTrackers
= true;
819 if (response
["torrents"]) {
820 let updateTorrentList
= false;
821 for (const key
in response
["torrents"]) {
822 if (!Object
.hasOwn(response
["torrents"], key
))
825 response
["torrents"][key
]["hash"] = key
;
826 response
["torrents"][key
]["rowId"] = key
;
827 if (response
["torrents"][key
]["state"])
828 response
["torrents"][key
]["status"] = response
["torrents"][key
]["state"];
829 torrentsTable
.updateRowData(response
["torrents"][key
]);
830 if (addTorrentToCategoryList(response
["torrents"][key
]))
831 update_categories
= true;
832 if (addTorrentToTagList(response
["torrents"][key
]))
834 if (response
["torrents"][key
]["name"])
835 updateTorrentList
= true;
838 if (updateTorrentList
)
839 setupCopyEventHandler();
841 if (response
["torrents_removed"]) {
842 response
["torrents_removed"].each((hash
) => {
843 torrentsTable
.removeRow(hash
);
844 removeTorrentFromCategoryList(hash
);
845 update_categories
= true; // Always to update All category
846 removeTorrentFromTagList(hash
);
847 updateTags
= true; // Always to update All tag
850 torrentsTable
.updateTable(full_update
);
851 if (response
["server_state"]) {
852 const tmp
= response
["server_state"];
853 for (const k
in tmp
) {
854 if (!Object
.hasOwn(tmp
, k
))
856 serverState
[k
] = tmp
[k
];
858 processServerState();
861 if (update_categories
) {
862 updateCategoryList();
863 window
.qBittorrent
.TransferList
.contextMenu
.updateCategoriesSubMenu(category_list
);
867 window
.qBittorrent
.TransferList
.contextMenu
.updateTagsSubMenu(tagList
);
873 // re-select previously selected rows
874 torrentsTable
.reselectRows(torrentsTableSelectedRows
);
876 syncRequestInProgress
= false;
877 syncData(window
.qBittorrent
.Client
.getSyncMainDataInterval());
880 syncRequestInProgress
= true;
884 updateMainData = function() {
885 torrentsTable
.updateTable();
889 const syncData = function(delay
) {
890 if (syncRequestInProgress
)
893 clearTimeout(syncMainDataTimeoutID
);
894 syncMainDataTimeoutID
= -1;
896 if (window
.qBittorrent
.Client
.isStopped())
899 syncMainDataTimeoutID
= syncMainData
.delay(delay
);
902 const processServerState = function() {
903 let transfer_info
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.dl_info_speed
, true);
904 if (serverState
.dl_rate_limit
> 0)
905 transfer_info
+= " [" + window
.qBittorrent
.Misc
.friendlyUnit(serverState
.dl_rate_limit
, true) + "]";
906 transfer_info
+= " (" + window
.qBittorrent
.Misc
.friendlyUnit(serverState
.dl_info_data
, false) + ")";
907 $("DlInfos").textContent
= transfer_info
;
908 transfer_info
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.up_info_speed
, true);
909 if (serverState
.up_rate_limit
> 0)
910 transfer_info
+= " [" + window
.qBittorrent
.Misc
.friendlyUnit(serverState
.up_rate_limit
, true) + "]";
911 transfer_info
+= " (" + window
.qBittorrent
.Misc
.friendlyUnit(serverState
.up_info_data
, false) + ")";
912 $("UpInfos").textContent
= transfer_info
;
914 document
.title
= (speedInTitle
915 ? (`QBT_TR([D: %1, U: %2])QBT_TR[CONTEXT=MainWindow] `
916 .replace("%1", window
.qBittorrent
.Misc
.friendlyUnit(serverState
.dl_info_speed
, true))
917 .replace("%2", window
.qBittorrent
.Misc
.friendlyUnit(serverState
.up_info_speed
, true)))
919 + window
.qBittorrent
.Client
.mainTitle();
921 $("freeSpaceOnDisk").textContent
= "QBT_TR(Free space: %1)QBT_TR[CONTEXT=HttpServer]".replace("%1", window
.qBittorrent
.Misc
.friendlyUnit(serverState
.free_space_on_disk
));
922 $("DHTNodes").textContent
= "QBT_TR(DHT: %1 nodes)QBT_TR[CONTEXT=StatusBar]".replace("%1", serverState
.dht_nodes
);
925 if (document
.getElementById("statisticsContent")) {
926 $("AlltimeDL").textContent
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.alltime_dl
, false);
927 $("AlltimeUL").textContent
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.alltime_ul
, false);
928 $("TotalWastedSession").textContent
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.total_wasted_session
, false);
929 $("GlobalRatio").textContent
= serverState
.global_ratio
;
930 $("TotalPeerConnections").textContent
= serverState
.total_peer_connections
;
931 $("ReadCacheHits").textContent
= serverState
.read_cache_hits
+ "%";
932 $("TotalBuffersSize").textContent
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.total_buffers_size
, false);
933 $("WriteCacheOverload").textContent
= serverState
.write_cache_overload
+ "%";
934 $("ReadCacheOverload").textContent
= serverState
.read_cache_overload
+ "%";
935 $("QueuedIOJobs").textContent
= serverState
.queued_io_jobs
;
936 $("AverageTimeInQueue").textContent
= serverState
.average_time_queue
+ " ms";
937 $("TotalQueuedSize").textContent
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.total_queued_size
, false);
940 switch (serverState
.connection_status
) {
942 $("connectionStatus").src
= "images/connected.svg";
943 $("connectionStatus").alt
= "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
944 $("connectionStatus").title
= "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
947 $("connectionStatus").src
= "images/firewalled.svg";
948 $("connectionStatus").alt
= "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
949 $("connectionStatus").title
= "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
952 $("connectionStatus").src
= "images/disconnected.svg";
953 $("connectionStatus").alt
= "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
954 $("connectionStatus").title
= "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
958 if (queueing_enabled
!== serverState
.queueing
) {
959 queueing_enabled
= serverState
.queueing
;
960 torrentsTable
.columns
["priority"].force_hide
= !queueing_enabled
;
961 torrentsTable
.updateColumn("priority");
962 if (queueing_enabled
) {
963 $("topQueuePosItem").removeClass("invisible");
964 $("increaseQueuePosItem").removeClass("invisible");
965 $("decreaseQueuePosItem").removeClass("invisible");
966 $("bottomQueuePosItem").removeClass("invisible");
967 $("queueingButtons").removeClass("invisible");
968 $("queueingMenuItems").removeClass("invisible");
971 $("topQueuePosItem").addClass("invisible");
972 $("increaseQueuePosItem").addClass("invisible");
973 $("decreaseQueuePosItem").addClass("invisible");
974 $("bottomQueuePosItem").addClass("invisible");
975 $("queueingButtons").addClass("invisible");
976 $("queueingMenuItems").addClass("invisible");
980 if (alternativeSpeedLimits
!== serverState
.use_alt_speed_limits
) {
981 alternativeSpeedLimits
= serverState
.use_alt_speed_limits
;
982 updateAltSpeedIcon(alternativeSpeedLimits
);
985 if (useSubcategories
!== serverState
.use_subcategories
) {
986 useSubcategories
= serverState
.use_subcategories
;
987 updateCategoryList();
990 serverSyncMainDataInterval
= Math
.max(serverState
.refresh_interval
, 500);
993 const updateAltSpeedIcon = function(enabled
) {
995 $("alternativeSpeedLimits").src
= "images/slow.svg";
996 $("alternativeSpeedLimits").alt
= "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
997 $("alternativeSpeedLimits").title
= "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
1000 $("alternativeSpeedLimits").src
= "images/slow_off.svg";
1001 $("alternativeSpeedLimits").alt
= "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
1002 $("alternativeSpeedLimits").title
= "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
1006 $("alternativeSpeedLimits").addEventListener("click", () => {
1007 // Change icon immediately to give some feedback
1008 updateAltSpeedIcon(!alternativeSpeedLimits
);
1011 url
: "api/v2/transfer/toggleSpeedLimitsMode",
1013 onComplete: function() {
1014 alternativeSpeedLimits
= !alternativeSpeedLimits
;
1017 onFailure: function() {
1018 // Restore icon in case of failure
1019 updateAltSpeedIcon(alternativeSpeedLimits
);
1024 $("DlInfos").addEventListener("click", () => { globalDownloadLimitFN(); });
1025 $("UpInfos").addEventListener("click", () => { globalUploadLimitFN(); });
1027 $("showTopToolbarLink").addEventListener("click", (e
) => {
1028 showTopToolbar
= !showTopToolbar
;
1029 LocalPreferences
.set("show_top_toolbar", showTopToolbar
.toString());
1030 if (showTopToolbar
) {
1031 $("showTopToolbarLink").firstChild
.style
.opacity
= "1";
1032 $("mochaToolbar").removeClass("invisible");
1035 $("showTopToolbarLink").firstChild
.style
.opacity
= "0";
1036 $("mochaToolbar").addClass("invisible");
1038 MochaUI
.Desktop
.setDesktopSize();
1041 $("showStatusBarLink").addEventListener("click", (e
) => {
1042 showStatusBar
= !showStatusBar
;
1043 LocalPreferences
.set("show_status_bar", showStatusBar
.toString());
1044 if (showStatusBar
) {
1045 $("showStatusBarLink").firstChild
.style
.opacity
= "1";
1046 $("desktopFooterWrapper").removeClass("invisible");
1049 $("showStatusBarLink").firstChild
.style
.opacity
= "0";
1050 $("desktopFooterWrapper").addClass("invisible");
1052 MochaUI
.Desktop
.setDesktopSize();
1055 const registerMagnetHandler = function() {
1056 if (typeof navigator
.registerProtocolHandler
!== "function") {
1057 if (window
.location
.protocol
!== "https:")
1058 alert("QBT_TR(To use this feature, the WebUI needs to be accessed over HTTPS)QBT_TR[CONTEXT=MainWindow]");
1060 alert("QBT_TR(Your browser does not support this feature)QBT_TR[CONTEXT=MainWindow]");
1064 const hashString
= location
.hash
? location
.hash
.replace(/^#/, "") : "";
1065 const hashParams
= new URLSearchParams(hashString
);
1066 hashParams
.set("download", "");
1068 const templateHashString
= hashParams
.toString().replace("download=", "download=%s");
1069 const templateUrl
= location
.origin
+ location
.pathname
1070 + location
.search
+ "#" + templateHashString
;
1072 navigator
.registerProtocolHandler("magnet", templateUrl
,
1073 "qBittorrent WebUI magnet handler");
1075 $("registerMagnetHandlerLink").addEventListener("click", (e
) => {
1076 registerMagnetHandler();
1079 $("showFiltersSidebarLink").addEventListener("click", (e
) => {
1080 showFiltersSidebar
= !showFiltersSidebar
;
1081 LocalPreferences
.set("show_filters_sidebar", showFiltersSidebar
.toString());
1082 if (showFiltersSidebar
) {
1083 $("showFiltersSidebarLink").firstChild
.style
.opacity
= "1";
1084 $("filtersColumn").removeClass("invisible");
1085 $("filtersColumn_handle").removeClass("invisible");
1088 $("showFiltersSidebarLink").firstChild
.style
.opacity
= "0";
1089 $("filtersColumn").addClass("invisible");
1090 $("filtersColumn_handle").addClass("invisible");
1092 MochaUI
.Desktop
.setDesktopSize();
1095 $("speedInBrowserTitleBarLink").addEventListener("click", (e
) => {
1096 speedInTitle
= !speedInTitle
;
1097 LocalPreferences
.set("speed_in_browser_title_bar", speedInTitle
.toString());
1099 $("speedInBrowserTitleBarLink").firstChild
.style
.opacity
= "1";
1101 $("speedInBrowserTitleBarLink").firstChild
.style
.opacity
= "0";
1102 processServerState();
1105 $("showSearchEngineLink").addEventListener("click", (e
) => {
1106 window
.qBittorrent
.Client
.showSearchEngine(!window
.qBittorrent
.Client
.isShowSearchEngine());
1107 LocalPreferences
.set("show_search_engine", window
.qBittorrent
.Client
.isShowSearchEngine().toString());
1111 $("showRssReaderLink").addEventListener("click", (e
) => {
1112 window
.qBittorrent
.Client
.showRssReader(!window
.qBittorrent
.Client
.isShowRssReader());
1113 LocalPreferences
.set("show_rss_reader", window
.qBittorrent
.Client
.isShowRssReader().toString());
1117 $("showLogViewerLink").addEventListener("click", (e
) => {
1118 window
.qBittorrent
.Client
.showLogViewer(!window
.qBittorrent
.Client
.isShowLogViewer());
1119 LocalPreferences
.set("show_log_viewer", window
.qBittorrent
.Client
.isShowLogViewer().toString());
1123 const updateTabDisplay = function() {
1124 if (window
.qBittorrent
.Client
.isShowRssReader()) {
1125 $("showRssReaderLink").firstChild
.style
.opacity
= "1";
1126 $("mainWindowTabs").removeClass("invisible");
1127 $("rssTabLink").removeClass("invisible");
1128 if (!MochaUI
.Panels
.instances
.RssPanel
)
1132 $("showRssReaderLink").firstChild
.style
.opacity
= "0";
1133 $("rssTabLink").addClass("invisible");
1134 if ($("rssTabLink").hasClass("selected"))
1135 $("transfersTabLink").click();
1138 if (window
.qBittorrent
.Client
.isShowSearchEngine()) {
1139 $("showSearchEngineLink").firstChild
.style
.opacity
= "1";
1140 $("mainWindowTabs").removeClass("invisible");
1141 $("searchTabLink").removeClass("invisible");
1142 if (!MochaUI
.Panels
.instances
.SearchPanel
)
1146 $("showSearchEngineLink").firstChild
.style
.opacity
= "0";
1147 $("searchTabLink").addClass("invisible");
1148 if ($("searchTabLink").hasClass("selected"))
1149 $("transfersTabLink").click();
1152 if (window
.qBittorrent
.Client
.isShowLogViewer()) {
1153 $("showLogViewerLink").firstChild
.style
.opacity
= "1";
1154 $("mainWindowTabs").removeClass("invisible");
1155 $("logTabLink").removeClass("invisible");
1156 if (!MochaUI
.Panels
.instances
.LogPanel
)
1160 $("showLogViewerLink").firstChild
.style
.opacity
= "0";
1161 $("logTabLink").addClass("invisible");
1162 if ($("logTabLink").hasClass("selected"))
1163 $("transfersTabLink").click();
1167 if (!window
.qBittorrent
.Client
.isShowRssReader() && !window
.qBittorrent
.Client
.isShowSearchEngine() && !window
.qBittorrent
.Client
.isShowLogViewer())
1168 $("mainWindowTabs").addClass("invisible");
1171 $("StatisticsLink").addEventListener("click", () => { StatisticsLinkFN(); });
1175 const showTransfersTab = function() {
1176 const showFiltersSidebar
= LocalPreferences
.get("show_filters_sidebar", "true") === "true";
1177 if (showFiltersSidebar
) {
1178 $("filtersColumn").removeClass("invisible");
1179 $("filtersColumn_handle").removeClass("invisible");
1181 $("mainColumn").removeClass("invisible");
1182 $("torrentsFilterToolbar").removeClass("invisible");
1184 customSyncMainDataInterval
= null;
1191 LocalPreferences
.set("selected_window_tab", "transfers");
1194 const hideTransfersTab = function() {
1195 $("filtersColumn").addClass("invisible");
1196 $("filtersColumn_handle").addClass("invisible");
1197 $("mainColumn").addClass("invisible");
1198 $("torrentsFilterToolbar").addClass("invisible");
1199 MochaUI
.Desktop
.resizePanels();
1202 const showSearchTab
= (function() {
1203 let searchTabInitialized
= false;
1206 // we must wait until the panel is fully loaded before proceeding.
1207 // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1208 // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1209 if (!isSearchPanelLoaded
) {
1216 if (!searchTabInitialized
) {
1217 window
.qBittorrent
.Search
.init();
1218 searchTabInitialized
= true;
1221 $("searchTabColumn").removeClass("invisible");
1222 customSyncMainDataInterval
= 30000;
1227 LocalPreferences
.set("selected_window_tab", "search");
1231 const hideSearchTab = function() {
1232 $("searchTabColumn").addClass("invisible");
1233 MochaUI
.Desktop
.resizePanels();
1236 const showRssTab
= (function() {
1237 let rssTabInitialized
= false;
1240 if (!rssTabInitialized
) {
1241 window
.qBittorrent
.Rss
.init();
1242 rssTabInitialized
= true;
1245 window
.qBittorrent
.Rss
.load();
1248 $("rssTabColumn").removeClass("invisible");
1249 customSyncMainDataInterval
= 30000;
1254 LocalPreferences
.set("selected_window_tab", "rss");
1258 const hideRssTab = function() {
1259 $("rssTabColumn").addClass("invisible");
1260 window
.qBittorrent
.Rss
&& window
.qBittorrent
.Rss
.unload();
1261 MochaUI
.Desktop
.resizePanels();
1264 const showLogTab
= (function() {
1265 let logTabInitialized
= false;
1268 // we must wait until the panel is fully loaded before proceeding.
1269 // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1270 // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1271 if (!isLogPanelLoaded
) {
1278 if (!logTabInitialized
) {
1279 window
.qBittorrent
.Log
.init();
1280 logTabInitialized
= true;
1283 window
.qBittorrent
.Log
.load();
1286 $("logTabColumn").removeClass("invisible");
1287 customSyncMainDataInterval
= 30000;
1292 LocalPreferences
.set("selected_window_tab", "log");
1296 const hideLogTab = function() {
1297 $("logTabColumn").addClass("invisible");
1298 MochaUI
.Desktop
.resizePanels();
1299 window
.qBittorrent
.Log
&& window
.qBittorrent
.Log
.unload();
1302 const addSearchPanel = function() {
1314 contentURL
: "views/search.html",
1316 js
: ["scripts/search.js"],
1318 isSearchPanelLoaded
= true;
1322 column
: "searchTabColumn",
1327 const addRssPanel = function() {
1339 contentURL
: "views/rss.html",
1341 column
: "rssTabColumn",
1346 const addLogPanel = function() {
1358 contentURL
: "views/log.html",
1360 css
: ["css/vanillaSelectBox.css"],
1361 js
: ["scripts/lib/vanillaSelectBox.js"],
1363 isLogPanelLoaded
= true;
1366 tabsURL
: "views/logTabs.html",
1367 tabsOnload: function() {
1368 MochaUI
.initializeTabs("panelTabs");
1370 $("logMessageLink").addEventListener("click", (e
) => {
1371 window
.qBittorrent
.Log
.setCurrentTab("main");
1374 $("logPeerLink").addEventListener("click", (e
) => {
1375 window
.qBittorrent
.Log
.setCurrentTab("peer");
1380 column
: "logTabColumn",
1385 const handleDownloadParam = function() {
1386 // Extract torrent URL from download param in WebUI URL hash
1387 const downloadHash
= "#download=";
1388 if (location
.hash
.indexOf(downloadHash
) !== 0)
1391 const url
= decodeURIComponent(location
.hash
.substring(downloadHash
.length
));
1392 // Remove the processed hash from the URL
1393 history
.replaceState("", document
.title
, (location
.pathname
+ location
.search
));
1394 showDownloadPage([url
]);
1408 contentURL
: "views/transferlist.html",
1409 onContentLoaded: function() {
1410 handleDownloadParam();
1413 column
: "mainColumn",
1414 onResize
: window
.qBittorrent
.Misc
.createDebounceHandler(500, (e
) => {
1419 let prop_h
= LocalPreferences
.get("properties_height_rel");
1420 if (prop_h
!== null)
1421 prop_h
= prop_h
.toFloat() * Window
.getSize().y
;
1423 prop_h
= Window
.getSize().y
/ 2.0;
1425 id
: "propertiesPanel",
1433 contentURL
: "views/properties.html",
1435 js
: ["scripts/prop-general.js", "scripts/prop-trackers.js", "scripts/prop-peers.js", "scripts/prop-webseeds.js", "scripts/prop-files.js"],
1436 onload: function() {
1437 updatePropertiesPanel = function() {
1438 switch (LocalPreferences
.get("selected_properties_tab")) {
1439 case "propGeneralLink":
1440 window
.qBittorrent
.PropGeneral
.updateData();
1442 case "propTrackersLink":
1443 window
.qBittorrent
.PropTrackers
.updateData();
1445 case "propPeersLink":
1446 window
.qBittorrent
.PropPeers
.updateData();
1448 case "propWebSeedsLink":
1449 window
.qBittorrent
.PropWebseeds
.updateData();
1451 case "propFilesLink":
1452 window
.qBittorrent
.PropFiles
.updateData();
1458 tabsURL
: "views/propertiesToolbar.html",
1459 tabsOnload: function() {}, // must be included, otherwise panel won't load properly
1460 onContentLoaded: function() {
1461 this.panelHeaderCollapseBoxEl
.classList
.add("invisible");
1463 const togglePropertiesPanel
= () => {
1464 this.collapseToggleEl
.click();
1465 LocalPreferences
.set("properties_panel_collapsed", this.isCollapsed
.toString());
1468 const selectTab
= (tabID
) => {
1469 const isAlreadySelected
= this.panelHeaderEl
.getElementById(tabID
).classList
.contains("selected");
1470 if (!isAlreadySelected
) {
1471 for (const tab
of this.panelHeaderEl
.getElementById("propertiesTabs").children
)
1472 tab
.classList
.toggle("selected", tab
.id
=== tabID
);
1474 const tabContentID
= tabID
.replace("Link", "");
1475 for (const tabContent
of this.contentEl
.children
)
1476 tabContent
.classList
.toggle("invisible", tabContent
.id
!== tabContentID
);
1478 LocalPreferences
.set("selected_properties_tab", tabID
);
1481 if (isAlreadySelected
|| this.isCollapsed
)
1482 togglePropertiesPanel();
1485 const lastUsedTab
= LocalPreferences
.get("selected_properties_tab", "propGeneralLink");
1486 selectTab(lastUsedTab
);
1488 const startCollapsed
= LocalPreferences
.get("properties_panel_collapsed", "false") === "true";
1490 togglePropertiesPanel();
1492 this.panelHeaderContentEl
.addEventListener("click", (e
) => {
1493 const selectedTab
= e
.target
.closest("li");
1497 selectTab(selectedTab
.id
);
1498 updatePropertiesPanel();
1500 const showFilesFilter
= (selectedTab
.id
=== "propFilesLink") && !this.isCollapsed
;
1501 document
.getElementById("torrentFilesFilterToolbar").classList
.toggle("invisible", !showFilesFilter
);
1504 column
: "mainColumn",
1508 // listen for changes to torrentsFilterInput
1509 let torrentsFilterInputTimer
= -1;
1510 $("torrentsFilterInput").addEventListener("input", () => {
1511 clearTimeout(torrentsFilterInputTimer
);
1512 torrentsFilterInputTimer
= setTimeout(() => {
1513 torrentsFilterInputTimer
= -1;
1514 torrentsTable
.updateTable();
1515 }, window
.qBittorrent
.Misc
.FILTER_INPUT_DELAY
);
1518 document
.getElementById("torrentsFilterToolbar").addEventListener("change", (e
) => { torrentsTable
.updateTable(); });
1520 $("transfersTabLink").addEventListener("click", () => { showTransfersTab(); });
1521 $("searchTabLink").addEventListener("click", () => { showSearchTab(); });
1522 $("rssTabLink").addEventListener("click", () => { showRssTab(); });
1523 $("logTabLink").addEventListener("click", () => { showLogTab(); });
1526 const registerDragAndDrop
= () => {
1527 $("desktop").addEventListener("dragover", (ev
) => {
1528 if (ev
.preventDefault
)
1529 ev
.preventDefault();
1532 $("desktop").addEventListener("dragenter", (ev
) => {
1533 if (ev
.preventDefault
)
1534 ev
.preventDefault();
1537 $("desktop").addEventListener("drop", (ev
) => {
1538 if (ev
.preventDefault
)
1539 ev
.preventDefault();
1541 const droppedFiles
= ev
.dataTransfer
.files
;
1543 if (droppedFiles
.length
> 0) {
1544 // dropped files or folders
1546 // can't handle folder due to cannot put the filelist (from dropped folder)
1547 // to <input> `files` field
1548 for (const item
of ev
.dataTransfer
.items
) {
1549 if (item
.webkitGetAsEntry().isDirectory
)
1553 const id
= "uploadPage";
1554 new MochaUI
.Window({
1556 icon
: "images/qbittorrent-tray.svg",
1557 title
: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
1558 loadMethod
: "iframe",
1559 contentURL
: new URI("upload.html").toString(),
1560 addClass
: "windowFrame", // fixes iframe scrolling on iOS Safari
1564 paddingHorizontal
: 0,
1565 width
: loadWindowWidth(id
, 500),
1566 height
: loadWindowHeight(id
, 460),
1567 onResize
: window
.qBittorrent
.Misc
.createDebounceHandler(500, (e
) => {
1570 onContentLoaded
: () => {
1571 const fileInput
= $(`${id}_iframe`).contentDocument
.getElementById("fileselect");
1572 fileInput
.files
= droppedFiles
;
1577 const droppedText
= ev
.dataTransfer
.getData("text");
1578 if (droppedText
.length
> 0) {
1581 const urls
= droppedText
.split("\n")
1582 .map((str
) => str
.trim())
1584 const lowercaseStr
= str
.toLowerCase();
1585 return lowercaseStr
.startsWith("http:")
1586 || lowercaseStr
.startsWith("https:")
1587 || lowercaseStr
.startsWith("magnet:")
1588 || ((str
.length
=== 40) && !(/[^0-9A-F]/i.test(str
))) // v1 hex-encoded SHA-1 info-hash
1589 || ((str
.length
=== 32) && !(/[^2-7A-Z]/i.test(str
))); // v1 Base32 encoded SHA-1 info-hash
1592 if (urls
.length
<= 0)
1595 const id
= "downloadPage";
1596 const contentURI
= new URI("download.html").setData("urls", urls
.map(encodeURIComponent
).join("|"));
1597 new MochaUI
.Window({
1599 icon
: "images/qbittorrent-tray.svg",
1600 title
: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
1601 loadMethod
: "iframe",
1602 contentURL
: contentURI
.toString(),
1603 addClass
: "windowFrame", // fixes iframe scrolling on iOS Safari
1608 paddingHorizontal
: 0,
1609 width
: loadWindowWidth(id
, 500),
1610 height
: loadWindowHeight(id
, 600),
1611 onResize
: window
.qBittorrent
.Misc
.createDebounceHandler(500, (e
) => {
1618 registerDragAndDrop();
1621 defaultEventType
: "keydown",
1623 "ctrl+a": function(event
) {
1624 if ((event
.target
.nodeName
=== "INPUT") || (event
.target
.nodeName
=== "TEXTAREA"))
1626 if (event
.target
.isContentEditable
)
1628 torrentsTable
.selectAll();
1629 event
.preventDefault();
1631 "delete": function(event
) {
1632 if ((event
.target
.nodeName
=== "INPUT") || (event
.target
.nodeName
=== "TEXTAREA"))
1634 if (event
.target
.isContentEditable
)
1637 event
.preventDefault();
1639 "shift+delete": (event
) => {
1640 if ((event
.target
.nodeName
=== "INPUT") || (event
.target
.nodeName
=== "TEXTAREA"))
1642 if (event
.target
.isContentEditable
)
1645 event
.preventDefault();
1651 window
.addEventListener("load", () => {
1652 // fetch various data and store it in memory
1653 window
.qBittorrent
.Cache
.buildInfo
.init();
1654 window
.qBittorrent
.Cache
.preferences
.init();
1655 window
.qBittorrent
.Cache
.qbtVersion
.init();
1657 // switch to previously used tab
1658 const previouslyUsedTab
= LocalPreferences
.get("selected_window_tab", "transfers");
1659 switch (previouslyUsedTab
) {
1661 if (window
.qBittorrent
.Client
.isShowSearchEngine())
1662 $("searchTabLink").click();
1665 if (window
.qBittorrent
.Client
.isShowRssReader())
1666 $("rssTabLink").click();
1669 if (window
.qBittorrent
.Client
.isShowLogViewer())
1670 $("logTabLink").click();
1673 $("transfersTabLink").click();
1676 console
.error(`Unexpected 'selected_window_tab' value: ${previouslyUsedTab}`);
1677 $("transfersTabLink").click();