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
,
34 getSyncMainDataInterval
: getSyncMainDataInterval
,
38 showSearchEngine
: showSearchEngine
,
39 showRssReader
: showRssReader
,
40 showLogViewer
: showLogViewer
,
41 isShowSearchEngine
: isShowSearchEngine
,
42 isShowRssReader
: isShowRssReader
,
43 isShowLogViewer
: isShowLogViewer
47 const closeWindow = function(windowID
) {
48 const window
= document
.getElementById(windowID
);
51 MochaUI
.closeWindow(window
);
54 const closeWindows = function() {
58 const getSyncMainDataInterval = function() {
59 return customSyncMainDataInterval
? customSyncMainDataInterval
: serverSyncMainDataInterval
;
63 const isStopped
= () => {
71 const mainTitle
= () => {
72 const emDash
= "\u2014";
73 const qbtVersion
= window
.qBittorrent
.Cache
.qbtVersion
.get();
74 const suffix
= window
.qBittorrent
.Cache
.preferences
.get()["app_instance_name"] || "";
75 const title
= `qBittorrent ${qbtVersion} QBT_TR(WebUI)QBT_TR[CONTEXT=OptionsDialog]`
76 + ((suffix
.length
> 0) ? ` ${emDash} ${suffix}` : "");
80 let showingSearchEngine
= false;
81 let showingRssReader
= false;
82 let showingLogViewer
= false;
84 const showSearchEngine = function(bool
) {
85 showingSearchEngine
= bool
;
87 const showRssReader = function(bool
) {
88 showingRssReader
= bool
;
90 const showLogViewer = function(bool
) {
91 showingLogViewer
= bool
;
93 const isShowSearchEngine = function() {
94 return showingSearchEngine
;
96 const isShowRssReader = function() {
97 return showingRssReader
;
99 const isShowLogViewer = function() {
100 return showingLogViewer
;
105 Object
.freeze(window
.qBittorrent
.Client
);
107 // TODO: move global functions/variables into some namespace/scope
109 this.torrentsTable
= new window
.qBittorrent
.DynamicTable
.TorrentsTable();
111 let updatePropertiesPanel = function() {};
113 this.updateMainData = function() {};
114 let alternativeSpeedLimits
= false;
115 let queueing_enabled
= true;
116 let serverSyncMainDataInterval
= 1500;
117 let customSyncMainDataInterval
= null;
118 let useSubcategories
= true;
119 const useAutoHideZeroStatusFilters
= LocalPreferences
.get("hide_zero_status_filters", "false") === "true";
120 const displayFullURLTrackerColumn
= LocalPreferences
.get("full_url_tracker_column", "false") === "true";
122 /* Categories filter */
123 const CATEGORIES_ALL
= 1;
124 const CATEGORIES_UNCATEGORIZED
= 2;
126 const category_list
= new Map();
128 let selectedCategory
= Number(LocalPreferences
.get("selected_category", CATEGORIES_ALL
));
129 let setCategoryFilter = function() {};
133 const TAGS_UNTAGGED
= 2;
135 const tagList
= new Map();
137 let selectedTag
= Number(LocalPreferences
.get("selected_tag", TAGS_ALL
));
138 let setTagFilter = function() {};
140 /* Trackers filter */
141 const TRACKERS_ALL
= 1;
142 const TRACKERS_TRACKERLESS
= 2;
144 /** @type Map<number, {host: string, trackerTorrentMap: Map<string, string[]>}> **/
145 const trackerList
= new Map();
147 let selectedTracker
= Number(LocalPreferences
.get("selected_tracker", TRACKERS_ALL
));
148 let setTrackerFilter = function() {};
151 let selectedStatus
= LocalPreferences
.get("selected_filter", "all");
152 let setStatusFilter = function() {};
153 let toggleFilterDisplay = function() {};
155 window
.addEventListener("DOMContentLoaded", () => {
156 let isSearchPanelLoaded
= false;
157 let isLogPanelLoaded
= false;
159 const saveColumnSizes = function() {
160 const filters_width
= $("Filters").getSize().x
;
161 LocalPreferences
.set("filters_width", filters_width
);
162 const properties_height_rel
= $("propertiesPanel").getSize().y
/ Window
.getSize().y
;
163 LocalPreferences
.set("properties_height_rel", properties_height_rel
);
166 window
.addEventListener("resize", window
.qBittorrent
.Misc
.createDebounceHandler(500, (e
) => {
167 // only save sizes if the columns are visible
168 if (!$("mainColumn").hasClass("invisible"))
172 /* MochaUI.Desktop = new MochaUI.Desktop();
173 MochaUI.Desktop.desktop.style.background = "#fff";
174 MochaUI.Desktop.desktop.style.visibility = "visible"; */
175 MochaUI
.Desktop
.initialize();
177 const buildTransfersTab = function() {
181 onResize
: window
.qBittorrent
.Misc
.createDebounceHandler(500, (e
) => {
184 width
: Number(LocalPreferences
.get("filters_width", 210)),
185 resizeLimit
: [1, 1000]
193 const buildSearchTab = function() {
195 id
: "searchTabColumn",
201 $("searchTabColumn").addClass("invisible");
204 const buildRssTab = function() {
212 $("rssTabColumn").addClass("invisible");
215 const buildLogTab = function() {
223 $("logTabColumn").addClass("invisible");
230 MochaUI
.initializeTabs("mainWindowTabsList");
232 setStatusFilter = function(name
) {
233 LocalPreferences
.set("selected_filter", name
);
234 selectedStatus
= name
;
235 highlightSelectedStatus();
239 setCategoryFilter = function(hash
) {
240 LocalPreferences
.set("selected_category", hash
);
241 selectedCategory
= Number(hash
);
242 highlightSelectedCategory();
246 setTagFilter = function(hash
) {
247 LocalPreferences
.set("selected_tag", hash
);
248 selectedTag
= Number(hash
);
249 highlightSelectedTag();
253 setTrackerFilter = function(hash
) {
254 LocalPreferences
.set("selected_tracker", hash
);
255 selectedTracker
= Number(hash
);
256 highlightSelectedTracker();
260 toggleFilterDisplay = function(filterListID
) {
261 const filterList
= document
.getElementById(filterListID
);
262 const filterTitle
= filterList
.previousElementSibling
;
263 const toggleIcon
= filterTitle
.firstElementChild
;
264 toggleIcon
.classList
.toggle("rotate");
265 LocalPreferences
.set(`filter_${filterListID.replace("FilterList", "")}_collapsed`, filterList
.classList
.toggle("invisible").toString());
279 contentURL
: "views/filters.html",
280 onContentLoaded: function() {
281 highlightSelectedStatus();
283 column
: "filtersColumn",
288 // Show Top Toolbar is enabled by default
289 let showTopToolbar
= LocalPreferences
.get("show_top_toolbar", "true") === "true";
290 if (!showTopToolbar
) {
291 $("showTopToolbarLink").firstChild
.style
.opacity
= "0";
292 $("mochaToolbar").addClass("invisible");
295 // Show Status Bar is enabled by default
296 let showStatusBar
= LocalPreferences
.get("show_status_bar", "true") === "true";
297 if (!showStatusBar
) {
298 $("showStatusBarLink").firstChild
.style
.opacity
= "0";
299 $("desktopFooterWrapper").addClass("invisible");
302 // Show Filters Sidebar is enabled by default
303 let showFiltersSidebar
= LocalPreferences
.get("show_filters_sidebar", "true") === "true";
304 if (!showFiltersSidebar
) {
305 $("showFiltersSidebarLink").firstChild
.style
.opacity
= "0";
306 $("filtersColumn").addClass("invisible");
307 $("filtersColumn_handle").addClass("invisible");
310 let speedInTitle
= LocalPreferences
.get("speed_in_browser_title_bar") === "true";
312 $("speedInBrowserTitleBarLink").firstChild
.style
.opacity
= "0";
314 // After showing/hiding the toolbar + status bar
315 window
.qBittorrent
.Client
.showSearchEngine(LocalPreferences
.get("show_search_engine") !== "false");
316 window
.qBittorrent
.Client
.showRssReader(LocalPreferences
.get("show_rss_reader") !== "false");
317 window
.qBittorrent
.Client
.showLogViewer(LocalPreferences
.get("show_log_viewer") === "true");
319 // After Show Top Toolbar
320 MochaUI
.Desktop
.setDesktopSize();
322 let syncMainDataLastResponseId
= 0;
323 const serverState
= {};
325 const removeTorrentFromCategoryList = function(hash
) {
330 category_list
.forEach((category
) => {
331 const deleteResult
= category
.torrents
.delete(hash
);
332 removed
||= deleteResult
;
338 const addTorrentToCategoryList = function(torrent
) {
339 const category
= torrent
["category"];
340 if (typeof category
=== "undefined")
343 const hash
= torrent
["hash"];
344 if (category
.length
=== 0) { // Empty category
345 removeTorrentFromCategoryList(hash
);
349 const categoryHash
= window
.qBittorrent
.Misc
.genHash(category
);
350 if (!category_list
.has(categoryHash
)) { // This should not happen
351 category_list
.set(categoryHash
, {
357 const torrents
= category_list
.get(categoryHash
).torrents
;
358 if (!torrents
.has(hash
)) {
359 removeTorrentFromCategoryList(hash
);
366 const removeTorrentFromTagList = function(hash
) {
371 tagList
.forEach((tag
) => {
372 const deleteResult
= tag
.torrents
.delete(hash
);
373 removed
||= deleteResult
;
379 const addTorrentToTagList = function(torrent
) {
380 if (torrent
["tags"] === undefined) // Tags haven't changed
383 const hash
= torrent
["hash"];
384 removeTorrentFromTagList(hash
);
386 if (torrent
["tags"].length
=== 0) // No tags
389 const tags
= torrent
["tags"].split(",");
391 for (let i
= 0; i
< tags
.length
; ++i
) {
392 const tagHash
= window
.qBittorrent
.Misc
.genHash(tags
[i
].trim());
393 if (!tagList
.has(tagHash
)) { // This should not happen
394 tagList
.set(tagHash
, {
400 const torrents
= tagList
.get(tagHash
).torrents
;
401 if (!torrents
.has(hash
)) {
409 const updateFilter = function(filter
, filterTitle
) {
410 const filterEl
= document
.getElementById(`${filter}_filter`);
411 const filterTorrentCount
= torrentsTable
.getFilteredTorrentsNumber(filter
, CATEGORIES_ALL
, TAGS_ALL
, TRACKERS_ALL
);
412 if (useAutoHideZeroStatusFilters
) {
413 const hideFilter
= (filterTorrentCount
=== 0) && (filter
!== "all");
414 if (filterEl
.classList
.toggle("invisible", hideFilter
))
417 filterEl
.firstElementChild
.lastChild
.nodeValue
= filterTitle
.replace("%1", filterTorrentCount
);
420 const updateFiltersList = function() {
421 updateFilter("all", "QBT_TR(All (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
422 updateFilter("downloading", "QBT_TR(Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
423 updateFilter("seeding", "QBT_TR(Seeding (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
424 updateFilter("completed", "QBT_TR(Completed (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
425 updateFilter("running", "QBT_TR(Running (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
426 updateFilter("stopped", "QBT_TR(Stopped (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
427 updateFilter("active", "QBT_TR(Active (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
428 updateFilter("inactive", "QBT_TR(Inactive (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
429 updateFilter("stalled", "QBT_TR(Stalled (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
430 updateFilter("stalled_uploading", "QBT_TR(Stalled Uploading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
431 updateFilter("stalled_downloading", "QBT_TR(Stalled Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
432 updateFilter("checking", "QBT_TR(Checking (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
433 updateFilter("moving", "QBT_TR(Moving (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
434 updateFilter("errored", "QBT_TR(Errored (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
437 const highlightSelectedStatus = function() {
438 const statusFilter
= document
.getElementById("statusFilterList");
439 const filterID
= `${selectedStatus}_filter`;
440 for (const status
of statusFilter
.children
)
441 status
.classList
.toggle("selectedFilter", (status
.id
=== filterID
));
444 const updateCategoryList = function() {
445 const categoryList
= document
.getElementById("categoryFilterList");
448 categoryList
.getChildren().each(c
=> c
.destroy());
450 const categoryItemTemplate
= document
.getElementById("categoryFilterItem");
452 const createCategoryLink
= (hash
, name
, count
) => {
453 const categoryFilterItem
= categoryItemTemplate
.content
.cloneNode(true).firstElementChild
;
454 categoryFilterItem
.id
= hash
;
455 categoryFilterItem
.classList
.toggle("selectedFilter", hash
=== selectedCategory
);
457 const span
= categoryFilterItem
.firstElementChild
;
458 span
.lastElementChild
.textContent
= `${name} (${count})`;
460 return categoryFilterItem
;
463 const createCategoryTree
= (category
) => {
464 const stack
= [{ parent
: categoriesFragment
, category
: category
}];
465 while (stack
.length
> 0) {
466 const { parent
, category
} = stack
.pop();
467 const displayName
= category
.nameSegments
.at(-1);
468 const listItem
= createCategoryLink(category
.categoryHash
, displayName
, category
.categoryCount
);
469 listItem
.firstElementChild
.style
.paddingLeft
= `${(category.nameSegments.length - 1) * 20 + 6}px`;
471 parent
.appendChild(listItem
);
473 if (category
.children
.length
> 0) {
474 listItem
.querySelector(".categoryToggle").style
.visibility
= "visible";
475 const unorderedList
= document
.createElement("ul");
476 listItem
.appendChild(unorderedList
);
477 for (const subcategory
of category
.children
.reverse())
478 stack
.push({ parent
: unorderedList
, category
: subcategory
});
480 const categoryLocalPref
= `category_${category.categoryHash}_collapsed`;
481 const isCollapsed
= !category
.forceExpand
&& (LocalPreferences
.get(categoryLocalPref
, "false") === "true");
482 LocalPreferences
.set(categoryLocalPref
, listItem
.classList
.toggle("collapsedCategory", isCollapsed
).toString());
486 const all
= torrentsTable
.getRowIds().length
;
487 let uncategorized
= 0;
488 for (const key
in torrentsTable
.rows
) {
489 if (!Object
.hasOwn(torrentsTable
.rows
, key
))
492 const row
= torrentsTable
.rows
[key
];
493 if (row
["full_data"].category
.length
=== 0)
497 const sortedCategories
= [];
498 category_list
.forEach((category
, hash
) => sortedCategories
.push({
499 categoryName
: category
.name
,
501 categoryCount
: category
.torrents
.size
,
502 nameSegments
: category
.name
.split("/"),
503 ...(useSubcategories
&& {
506 forceExpand
: LocalPreferences
.get(`category_${hash}_collapsed`) === null
509 sortedCategories
.sort((left
, right
) => {
510 const leftSegments
= left
.nameSegments
;
511 const rightSegments
= right
.nameSegments
;
513 for (let i
= 0, iMax
= Math
.min(leftSegments
.length
, rightSegments
.length
); i
< iMax
; ++i
) {
514 const compareResult
= window
.qBittorrent
.Misc
.naturalSortCollator
.compare(
515 leftSegments
[i
], rightSegments
[i
]);
516 if (compareResult
!== 0)
517 return compareResult
;
520 return leftSegments
.length
- rightSegments
.length
;
523 const categoriesFragment
= new DocumentFragment();
524 categoriesFragment
.appendChild(createCategoryLink(CATEGORIES_ALL
, "QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]", all
));
525 categoriesFragment
.appendChild(createCategoryLink(CATEGORIES_UNCATEGORIZED
, "QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]", uncategorized
));
527 if (useSubcategories
) {
528 categoryList
.classList
.add("subcategories");
529 for (let i
= 0; i
< sortedCategories
.length
; ++i
) {
530 const category
= sortedCategories
[i
];
531 for (let j
= (i
+ 1);
532 ((j
< sortedCategories
.length
) && sortedCategories
[j
].categoryName
.startsWith(`${category.categoryName}/`)); ++j
) {
533 const subcategory
= sortedCategories
[j
];
534 category
.categoryCount
+= subcategory
.categoryCount
;
535 category
.forceExpand
||= subcategory
.forceExpand
;
537 const isDirectSubcategory
= (subcategory
.nameSegments
.length
- category
.nameSegments
.length
) === 1;
538 if (isDirectSubcategory
) {
539 subcategory
.parentID
= category
.categoryHash
;
540 category
.children
.push(subcategory
);
544 for (const category
of sortedCategories
) {
545 if (category
.parentID
=== null)
546 createCategoryTree(category
);
550 categoryList
.classList
.remove("subcategories");
551 for (const { categoryHash
, categoryName
, categoryCount
} of sortedCategories
)
552 categoriesFragment
.appendChild(createCategoryLink(categoryHash
, categoryName
, categoryCount
));
555 categoryList
.appendChild(categoriesFragment
);
556 window
.qBittorrent
.Filters
.categoriesFilterContextMenu
.searchAndAddTargets();
559 const highlightSelectedCategory = function() {
560 const categoryList
= document
.getElementById("categoryFilterList");
564 for (const category
of categoryList
.getElementsByTagName("li"))
565 category
.classList
.toggle("selectedFilter", (Number(category
.id
) === selectedCategory
));
568 const updateTagList = function() {
569 const tagFilterList
= $("tagFilterList");
570 if (tagFilterList
=== null)
573 tagFilterList
.getChildren().each(c
=> c
.destroy());
575 const tagItemTemplate
= document
.getElementById("tagFilterItem");
577 const createLink = function(hash
, text
, count
) {
578 const tagFilterItem
= tagItemTemplate
.content
.cloneNode(true).firstElementChild
;
579 tagFilterItem
.id
= hash
;
580 tagFilterItem
.classList
.toggle("selectedFilter", hash
=== selectedTag
);
582 const span
= tagFilterItem
.firstElementChild
;
583 span
.lastChild
.textContent
= `${text} (${count})`;
585 return tagFilterItem
;
588 const torrentsCount
= torrentsTable
.getRowIds().length
;
590 for (const key
in torrentsTable
.rows
) {
591 if (Object
.hasOwn(torrentsTable
.rows
, key
) && (torrentsTable
.rows
[key
]["full_data"].tags
.length
=== 0))
594 tagFilterList
.appendChild(createLink(TAGS_ALL
, "QBT_TR(All)QBT_TR[CONTEXT=TagFilterModel]", torrentsCount
));
595 tagFilterList
.appendChild(createLink(TAGS_UNTAGGED
, "QBT_TR(Untagged)QBT_TR[CONTEXT=TagFilterModel]", untagged
));
597 const sortedTags
= [];
598 tagList
.forEach((tag
, hash
) => sortedTags
.push({
601 tagSize
: tag
.torrents
.size
603 sortedTags
.sort((left
, right
) => window
.qBittorrent
.Misc
.naturalSortCollator
.compare(left
.tagName
, right
.tagName
));
605 for (const { tagName
, tagHash
, tagSize
} of sortedTags
)
606 tagFilterList
.appendChild(createLink(tagHash
, tagName
, tagSize
));
608 window
.qBittorrent
.Filters
.tagsFilterContextMenu
.searchAndAddTargets();
611 const highlightSelectedTag = function() {
612 const tagFilterList
= document
.getElementById("tagFilterList");
616 for (const tag
of tagFilterList
.children
)
617 tag
.classList
.toggle("selectedFilter", (Number(tag
.id
) === selectedTag
));
620 const updateTrackerList = function() {
621 const trackerFilterList
= $("trackerFilterList");
622 if (trackerFilterList
=== null)
625 trackerFilterList
.getChildren().each(c
=> c
.destroy());
627 const trackerItemTemplate
= document
.getElementById("trackerFilterItem");
629 const createLink = function(hash
, text
, count
) {
630 const trackerFilterItem
= trackerItemTemplate
.content
.cloneNode(true).firstElementChild
;
631 trackerFilterItem
.id
= hash
;
632 trackerFilterItem
.classList
.toggle("selectedFilter", hash
=== selectedTracker
);
634 const span
= trackerFilterItem
.firstElementChild
;
635 span
.lastChild
.textContent
= text
.replace("%1", count
);
637 return trackerFilterItem
;
640 const torrentsCount
= torrentsTable
.getRowIds().length
;
641 trackerFilterList
.appendChild(createLink(TRACKERS_ALL
, "QBT_TR(All (%1))QBT_TR[CONTEXT=TrackerFiltersList]", torrentsCount
));
642 let trackerlessTorrentsCount
= 0;
643 for (const key
in torrentsTable
.rows
) {
644 if (Object
.hasOwn(torrentsTable
.rows
, key
) && (torrentsTable
.rows
[key
]["full_data"].trackers_count
=== 0))
645 trackerlessTorrentsCount
+= 1;
647 trackerFilterList
.appendChild(createLink(TRACKERS_TRACKERLESS
, "QBT_TR(Trackerless (%1))QBT_TR[CONTEXT=TrackerFiltersList]", trackerlessTorrentsCount
));
649 // Sort trackers by hostname
650 const sortedList
= [];
651 trackerList
.forEach(({ host
, trackerTorrentMap
}, hash
) => {
652 const uniqueTorrents
= new Set();
653 for (const torrents
of trackerTorrentMap
.values()) {
654 for (const torrent
of torrents
)
655 uniqueTorrents
.add(torrent
);
661 trackerCount
: uniqueTorrents
.size
,
664 sortedList
.sort((left
, right
) => window
.qBittorrent
.Misc
.naturalSortCollator
.compare(left
.trackerHost
, right
.trackerHost
));
665 for (const { trackerHost
, trackerHash
, trackerCount
} of sortedList
)
666 trackerFilterList
.appendChild(createLink(trackerHash
, (trackerHost
+ " (%1)"), trackerCount
));
668 window
.qBittorrent
.Filters
.trackersFilterContextMenu
.searchAndAddTargets();
671 const highlightSelectedTracker = function() {
672 const trackerFilterList
= document
.getElementById("trackerFilterList");
673 if (!trackerFilterList
)
676 for (const tracker
of trackerFilterList
.children
)
677 tracker
.classList
.toggle("selectedFilter", (Number(tracker
.id
) === selectedTracker
));
680 const setupCopyEventHandler
= (function() {
685 clipboardEvent
.destroy();
687 clipboardEvent
= new ClipboardJS(".copyToClipboard", {
688 text: function(trigger
) {
689 switch (trigger
.id
) {
692 case "copyInfohash1":
693 return copyInfohashFN(1);
694 case "copyInfohash2":
695 return copyInfohashFN(2);
696 case "copyMagnetLink":
697 return copyMagnetLinkFN();
701 return copyCommentFN();
710 let syncMainDataTimeoutID
= -1;
711 let syncRequestInProgress
= false;
712 const syncMainData = function() {
713 const url
= new URI("api/v2/sync/maindata");
714 url
.setData("rid", syncMainDataLastResponseId
);
715 const request
= new Request
.JSON({
719 onFailure: function() {
720 const errorDiv
= $("error_div");
722 errorDiv
.textContent
= "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]";
723 syncRequestInProgress
= false;
726 onSuccess: function(response
) {
727 $("error_div").textContent
= "";
729 clearTimeout(torrentsFilterInputTimer
);
730 torrentsFilterInputTimer
= -1;
732 let torrentsTableSelectedRows
;
733 let update_categories
= false;
734 let updateTags
= false;
735 let updateTrackers
= false;
736 const full_update
= (response
["full_update"] === true);
738 torrentsTableSelectedRows
= torrentsTable
.selectedRowsIds();
739 update_categories
= true;
741 updateTrackers
= true;
742 torrentsTable
.clear();
743 category_list
.clear();
748 syncMainDataLastResponseId
= response
["rid"];
749 if (response
["categories"]) {
750 for (const key
in response
["categories"]) {
751 if (!Object
.hasOwn(response
["categories"], key
))
754 const responseCategory
= response
["categories"][key
];
755 const categoryHash
= window
.qBittorrent
.Misc
.genHash(key
);
756 const category
= category_list
.get(categoryHash
);
757 if (category
!== undefined) {
758 // only the save path can change for existing categories
759 category
.savePath
= responseCategory
.savePath
;
762 category_list
.set(categoryHash
, {
763 name
: responseCategory
.name
,
764 savePath
: responseCategory
.savePath
,
769 update_categories
= true;
771 if (response
["categories_removed"]) {
772 response
["categories_removed"].each((category
) => {
773 const categoryHash
= window
.qBittorrent
.Misc
.genHash(category
);
774 category_list
.delete(categoryHash
);
776 update_categories
= true;
778 if (response
["tags"]) {
779 for (const tag
of response
["tags"]) {
780 const tagHash
= window
.qBittorrent
.Misc
.genHash(tag
);
781 if (!tagList
.has(tagHash
)) {
782 tagList
.set(tagHash
, {
790 if (response
["tags_removed"]) {
791 for (let i
= 0; i
< response
["tags_removed"].length
; ++i
) {
792 const tagHash
= window
.qBittorrent
.Misc
.genHash(response
["tags_removed"][i
]);
793 tagList
.delete(tagHash
);
797 if (response
["trackers"]) {
798 for (const [tracker
, torrents
] of Object
.entries(response
["trackers"])) {
799 const host
= window
.qBittorrent
.Misc
.getHost(tracker
);
800 const hash
= window
.qBittorrent
.Misc
.genHash(host
);
802 let trackerListItem
= trackerList
.get(hash
);
803 if (trackerListItem
=== undefined) {
804 trackerListItem
= { host
: host
, trackerTorrentMap
: new Map() };
805 trackerList
.set(hash
, trackerListItem
);
808 trackerListItem
.trackerTorrentMap
.set(tracker
, [...torrents
]);
810 updateTrackers
= true;
812 if (response
["trackers_removed"]) {
813 for (let i
= 0; i
< response
["trackers_removed"].length
; ++i
) {
814 const tracker
= response
["trackers_removed"][i
];
815 const host
= window
.qBittorrent
.Misc
.getHost(tracker
);
816 const hash
= window
.qBittorrent
.Misc
.genHash(host
);
817 const trackerListEntry
= trackerList
.get(hash
);
818 if (trackerListEntry
)
819 trackerListEntry
.trackerTorrentMap
.delete(tracker
);
821 updateTrackers
= true;
823 if (response
["torrents"]) {
824 let updateTorrentList
= false;
825 for (const key
in response
["torrents"]) {
826 if (!Object
.hasOwn(response
["torrents"], key
))
829 response
["torrents"][key
]["hash"] = key
;
830 response
["torrents"][key
]["rowId"] = key
;
831 if (response
["torrents"][key
]["state"])
832 response
["torrents"][key
]["status"] = response
["torrents"][key
]["state"];
833 torrentsTable
.updateRowData(response
["torrents"][key
]);
834 if (addTorrentToCategoryList(response
["torrents"][key
]))
835 update_categories
= true;
836 if (addTorrentToTagList(response
["torrents"][key
]))
838 if (response
["torrents"][key
]["name"])
839 updateTorrentList
= true;
842 if (updateTorrentList
)
843 setupCopyEventHandler();
845 if (response
["torrents_removed"]) {
846 response
["torrents_removed"].each((hash
) => {
847 torrentsTable
.removeRow(hash
);
848 removeTorrentFromCategoryList(hash
);
849 update_categories
= true; // Always to update All category
850 removeTorrentFromTagList(hash
);
851 updateTags
= true; // Always to update All tag
854 torrentsTable
.updateTable(full_update
);
855 if (response
["server_state"]) {
856 const tmp
= response
["server_state"];
857 for (const k
in tmp
) {
858 if (!Object
.hasOwn(tmp
, k
))
860 serverState
[k
] = tmp
[k
];
862 processServerState();
865 if (update_categories
) {
866 updateCategoryList();
867 window
.qBittorrent
.TransferList
.contextMenu
.updateCategoriesSubMenu(category_list
);
871 window
.qBittorrent
.TransferList
.contextMenu
.updateTagsSubMenu(tagList
);
877 // re-select previously selected rows
878 torrentsTable
.reselectRows(torrentsTableSelectedRows
);
880 syncRequestInProgress
= false;
881 syncData(window
.qBittorrent
.Client
.getSyncMainDataInterval());
884 syncRequestInProgress
= true;
888 updateMainData = function() {
889 torrentsTable
.updateTable();
893 const syncData = function(delay
) {
894 if (syncRequestInProgress
)
897 clearTimeout(syncMainDataTimeoutID
);
898 syncMainDataTimeoutID
= -1;
900 if (window
.qBittorrent
.Client
.isStopped())
903 syncMainDataTimeoutID
= syncMainData
.delay(delay
);
906 const processServerState = function() {
907 let transfer_info
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.dl_info_speed
, true);
908 if (serverState
.dl_rate_limit
> 0)
909 transfer_info
+= " [" + window
.qBittorrent
.Misc
.friendlyUnit(serverState
.dl_rate_limit
, true) + "]";
910 transfer_info
+= " (" + window
.qBittorrent
.Misc
.friendlyUnit(serverState
.dl_info_data
, false) + ")";
911 $("DlInfos").textContent
= transfer_info
;
912 transfer_info
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.up_info_speed
, true);
913 if (serverState
.up_rate_limit
> 0)
914 transfer_info
+= " [" + window
.qBittorrent
.Misc
.friendlyUnit(serverState
.up_rate_limit
, true) + "]";
915 transfer_info
+= " (" + window
.qBittorrent
.Misc
.friendlyUnit(serverState
.up_info_data
, false) + ")";
916 $("UpInfos").textContent
= transfer_info
;
918 document
.title
= (speedInTitle
919 ? (`QBT_TR([D: %1, U: %2])QBT_TR[CONTEXT=MainWindow] `
920 .replace("%1", window
.qBittorrent
.Misc
.friendlyUnit(serverState
.dl_info_speed
, true))
921 .replace("%2", window
.qBittorrent
.Misc
.friendlyUnit(serverState
.up_info_speed
, true)))
923 + window
.qBittorrent
.Client
.mainTitle();
925 $("freeSpaceOnDisk").textContent
= "QBT_TR(Free space: %1)QBT_TR[CONTEXT=HttpServer]".replace("%1", window
.qBittorrent
.Misc
.friendlyUnit(serverState
.free_space_on_disk
));
926 $("DHTNodes").textContent
= "QBT_TR(DHT: %1 nodes)QBT_TR[CONTEXT=StatusBar]".replace("%1", serverState
.dht_nodes
);
929 if (document
.getElementById("statisticsContent")) {
930 $("AlltimeDL").textContent
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.alltime_dl
, false);
931 $("AlltimeUL").textContent
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.alltime_ul
, false);
932 $("TotalWastedSession").textContent
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.total_wasted_session
, false);
933 $("GlobalRatio").textContent
= serverState
.global_ratio
;
934 $("TotalPeerConnections").textContent
= serverState
.total_peer_connections
;
935 $("ReadCacheHits").textContent
= serverState
.read_cache_hits
+ "%";
936 $("TotalBuffersSize").textContent
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.total_buffers_size
, false);
937 $("WriteCacheOverload").textContent
= serverState
.write_cache_overload
+ "%";
938 $("ReadCacheOverload").textContent
= serverState
.read_cache_overload
+ "%";
939 $("QueuedIOJobs").textContent
= serverState
.queued_io_jobs
;
940 $("AverageTimeInQueue").textContent
= serverState
.average_time_queue
+ " ms";
941 $("TotalQueuedSize").textContent
= window
.qBittorrent
.Misc
.friendlyUnit(serverState
.total_queued_size
, false);
944 switch (serverState
.connection_status
) {
946 $("connectionStatus").src
= "images/connected.svg";
947 $("connectionStatus").alt
= "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
948 $("connectionStatus").title
= "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
951 $("connectionStatus").src
= "images/firewalled.svg";
952 $("connectionStatus").alt
= "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
953 $("connectionStatus").title
= "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
956 $("connectionStatus").src
= "images/disconnected.svg";
957 $("connectionStatus").alt
= "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
958 $("connectionStatus").title
= "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
962 if (queueing_enabled
!== serverState
.queueing
) {
963 queueing_enabled
= serverState
.queueing
;
964 torrentsTable
.columns
["priority"].force_hide
= !queueing_enabled
;
965 torrentsTable
.updateColumn("priority");
966 if (queueing_enabled
) {
967 $("topQueuePosItem").removeClass("invisible");
968 $("increaseQueuePosItem").removeClass("invisible");
969 $("decreaseQueuePosItem").removeClass("invisible");
970 $("bottomQueuePosItem").removeClass("invisible");
971 $("queueingButtons").removeClass("invisible");
972 $("queueingMenuItems").removeClass("invisible");
975 $("topQueuePosItem").addClass("invisible");
976 $("increaseQueuePosItem").addClass("invisible");
977 $("decreaseQueuePosItem").addClass("invisible");
978 $("bottomQueuePosItem").addClass("invisible");
979 $("queueingButtons").addClass("invisible");
980 $("queueingMenuItems").addClass("invisible");
984 if (alternativeSpeedLimits
!== serverState
.use_alt_speed_limits
) {
985 alternativeSpeedLimits
= serverState
.use_alt_speed_limits
;
986 updateAltSpeedIcon(alternativeSpeedLimits
);
989 if (useSubcategories
!== serverState
.use_subcategories
) {
990 useSubcategories
= serverState
.use_subcategories
;
991 updateCategoryList();
994 serverSyncMainDataInterval
= Math
.max(serverState
.refresh_interval
, 500);
997 const updateAltSpeedIcon = function(enabled
) {
999 $("alternativeSpeedLimits").src
= "images/slow.svg";
1000 $("alternativeSpeedLimits").alt
= "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
1001 $("alternativeSpeedLimits").title
= "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
1004 $("alternativeSpeedLimits").src
= "images/slow_off.svg";
1005 $("alternativeSpeedLimits").alt
= "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
1006 $("alternativeSpeedLimits").title
= "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
1010 $("alternativeSpeedLimits").addEventListener("click", () => {
1011 // Change icon immediately to give some feedback
1012 updateAltSpeedIcon(!alternativeSpeedLimits
);
1015 url
: "api/v2/transfer/toggleSpeedLimitsMode",
1017 onComplete: function() {
1018 alternativeSpeedLimits
= !alternativeSpeedLimits
;
1021 onFailure: function() {
1022 // Restore icon in case of failure
1023 updateAltSpeedIcon(alternativeSpeedLimits
);
1028 $("DlInfos").addEventListener("click", () => { globalDownloadLimitFN(); });
1029 $("UpInfos").addEventListener("click", () => { globalUploadLimitFN(); });
1031 $("showTopToolbarLink").addEventListener("click", (e
) => {
1032 showTopToolbar
= !showTopToolbar
;
1033 LocalPreferences
.set("show_top_toolbar", showTopToolbar
.toString());
1034 if (showTopToolbar
) {
1035 $("showTopToolbarLink").firstChild
.style
.opacity
= "1";
1036 $("mochaToolbar").removeClass("invisible");
1039 $("showTopToolbarLink").firstChild
.style
.opacity
= "0";
1040 $("mochaToolbar").addClass("invisible");
1042 MochaUI
.Desktop
.setDesktopSize();
1045 $("showStatusBarLink").addEventListener("click", (e
) => {
1046 showStatusBar
= !showStatusBar
;
1047 LocalPreferences
.set("show_status_bar", showStatusBar
.toString());
1048 if (showStatusBar
) {
1049 $("showStatusBarLink").firstChild
.style
.opacity
= "1";
1050 $("desktopFooterWrapper").removeClass("invisible");
1053 $("showStatusBarLink").firstChild
.style
.opacity
= "0";
1054 $("desktopFooterWrapper").addClass("invisible");
1056 MochaUI
.Desktop
.setDesktopSize();
1059 const registerMagnetHandler = function() {
1060 if (typeof navigator
.registerProtocolHandler
!== "function") {
1061 if (window
.location
.protocol
!== "https:")
1062 alert("QBT_TR(To use this feature, the WebUI needs to be accessed over HTTPS)QBT_TR[CONTEXT=MainWindow]");
1064 alert("QBT_TR(Your browser does not support this feature)QBT_TR[CONTEXT=MainWindow]");
1068 const hashString
= location
.hash
? location
.hash
.replace(/^#/, "") : "";
1069 const hashParams
= new URLSearchParams(hashString
);
1070 hashParams
.set("download", "");
1072 const templateHashString
= hashParams
.toString().replace("download=", "download=%s");
1073 const templateUrl
= location
.origin
+ location
.pathname
1074 + location
.search
+ "#" + templateHashString
;
1076 navigator
.registerProtocolHandler("magnet", templateUrl
,
1077 "qBittorrent WebUI magnet handler");
1079 $("registerMagnetHandlerLink").addEventListener("click", (e
) => {
1080 registerMagnetHandler();
1083 $("showFiltersSidebarLink").addEventListener("click", (e
) => {
1084 showFiltersSidebar
= !showFiltersSidebar
;
1085 LocalPreferences
.set("show_filters_sidebar", showFiltersSidebar
.toString());
1086 if (showFiltersSidebar
) {
1087 $("showFiltersSidebarLink").firstChild
.style
.opacity
= "1";
1088 $("filtersColumn").removeClass("invisible");
1089 $("filtersColumn_handle").removeClass("invisible");
1092 $("showFiltersSidebarLink").firstChild
.style
.opacity
= "0";
1093 $("filtersColumn").addClass("invisible");
1094 $("filtersColumn_handle").addClass("invisible");
1096 MochaUI
.Desktop
.setDesktopSize();
1099 $("speedInBrowserTitleBarLink").addEventListener("click", (e
) => {
1100 speedInTitle
= !speedInTitle
;
1101 LocalPreferences
.set("speed_in_browser_title_bar", speedInTitle
.toString());
1103 $("speedInBrowserTitleBarLink").firstChild
.style
.opacity
= "1";
1105 $("speedInBrowserTitleBarLink").firstChild
.style
.opacity
= "0";
1106 processServerState();
1109 $("showSearchEngineLink").addEventListener("click", (e
) => {
1110 window
.qBittorrent
.Client
.showSearchEngine(!window
.qBittorrent
.Client
.isShowSearchEngine());
1111 LocalPreferences
.set("show_search_engine", window
.qBittorrent
.Client
.isShowSearchEngine().toString());
1115 $("showRssReaderLink").addEventListener("click", (e
) => {
1116 window
.qBittorrent
.Client
.showRssReader(!window
.qBittorrent
.Client
.isShowRssReader());
1117 LocalPreferences
.set("show_rss_reader", window
.qBittorrent
.Client
.isShowRssReader().toString());
1121 $("showLogViewerLink").addEventListener("click", (e
) => {
1122 window
.qBittorrent
.Client
.showLogViewer(!window
.qBittorrent
.Client
.isShowLogViewer());
1123 LocalPreferences
.set("show_log_viewer", window
.qBittorrent
.Client
.isShowLogViewer().toString());
1127 const updateTabDisplay = function() {
1128 if (window
.qBittorrent
.Client
.isShowRssReader()) {
1129 $("showRssReaderLink").firstChild
.style
.opacity
= "1";
1130 $("mainWindowTabs").removeClass("invisible");
1131 $("rssTabLink").removeClass("invisible");
1132 if (!MochaUI
.Panels
.instances
.RssPanel
)
1136 $("showRssReaderLink").firstChild
.style
.opacity
= "0";
1137 $("rssTabLink").addClass("invisible");
1138 if ($("rssTabLink").hasClass("selected"))
1139 $("transfersTabLink").click();
1142 if (window
.qBittorrent
.Client
.isShowSearchEngine()) {
1143 $("showSearchEngineLink").firstChild
.style
.opacity
= "1";
1144 $("mainWindowTabs").removeClass("invisible");
1145 $("searchTabLink").removeClass("invisible");
1146 if (!MochaUI
.Panels
.instances
.SearchPanel
)
1150 $("showSearchEngineLink").firstChild
.style
.opacity
= "0";
1151 $("searchTabLink").addClass("invisible");
1152 if ($("searchTabLink").hasClass("selected"))
1153 $("transfersTabLink").click();
1156 if (window
.qBittorrent
.Client
.isShowLogViewer()) {
1157 $("showLogViewerLink").firstChild
.style
.opacity
= "1";
1158 $("mainWindowTabs").removeClass("invisible");
1159 $("logTabLink").removeClass("invisible");
1160 if (!MochaUI
.Panels
.instances
.LogPanel
)
1164 $("showLogViewerLink").firstChild
.style
.opacity
= "0";
1165 $("logTabLink").addClass("invisible");
1166 if ($("logTabLink").hasClass("selected"))
1167 $("transfersTabLink").click();
1171 if (!window
.qBittorrent
.Client
.isShowRssReader() && !window
.qBittorrent
.Client
.isShowSearchEngine() && !window
.qBittorrent
.Client
.isShowLogViewer())
1172 $("mainWindowTabs").addClass("invisible");
1175 $("StatisticsLink").addEventListener("click", () => { StatisticsLinkFN(); });
1179 const showTransfersTab = function() {
1180 const showFiltersSidebar
= LocalPreferences
.get("show_filters_sidebar", "true") === "true";
1181 if (showFiltersSidebar
) {
1182 $("filtersColumn").removeClass("invisible");
1183 $("filtersColumn_handle").removeClass("invisible");
1185 $("mainColumn").removeClass("invisible");
1186 $("torrentsFilterToolbar").removeClass("invisible");
1188 customSyncMainDataInterval
= null;
1195 LocalPreferences
.set("selected_window_tab", "transfers");
1198 const hideTransfersTab = function() {
1199 $("filtersColumn").addClass("invisible");
1200 $("filtersColumn_handle").addClass("invisible");
1201 $("mainColumn").addClass("invisible");
1202 $("torrentsFilterToolbar").addClass("invisible");
1203 MochaUI
.Desktop
.resizePanels();
1206 const showSearchTab
= (function() {
1207 let searchTabInitialized
= false;
1210 // we must wait until the panel is fully loaded before proceeding.
1211 // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1212 // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1213 if (!isSearchPanelLoaded
) {
1220 if (!searchTabInitialized
) {
1221 window
.qBittorrent
.Search
.init();
1222 searchTabInitialized
= true;
1225 $("searchTabColumn").removeClass("invisible");
1226 customSyncMainDataInterval
= 30000;
1231 LocalPreferences
.set("selected_window_tab", "search");
1235 const hideSearchTab = function() {
1236 $("searchTabColumn").addClass("invisible");
1237 MochaUI
.Desktop
.resizePanels();
1240 const showRssTab
= (function() {
1241 let rssTabInitialized
= false;
1244 if (!rssTabInitialized
) {
1245 window
.qBittorrent
.Rss
.init();
1246 rssTabInitialized
= true;
1249 window
.qBittorrent
.Rss
.load();
1252 $("rssTabColumn").removeClass("invisible");
1253 customSyncMainDataInterval
= 30000;
1258 LocalPreferences
.set("selected_window_tab", "rss");
1262 const hideRssTab = function() {
1263 $("rssTabColumn").addClass("invisible");
1264 window
.qBittorrent
.Rss
&& window
.qBittorrent
.Rss
.unload();
1265 MochaUI
.Desktop
.resizePanels();
1268 const showLogTab
= (function() {
1269 let logTabInitialized
= false;
1272 // we must wait until the panel is fully loaded before proceeding.
1273 // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1274 // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1275 if (!isLogPanelLoaded
) {
1282 if (!logTabInitialized
) {
1283 window
.qBittorrent
.Log
.init();
1284 logTabInitialized
= true;
1287 window
.qBittorrent
.Log
.load();
1290 $("logTabColumn").removeClass("invisible");
1291 customSyncMainDataInterval
= 30000;
1296 LocalPreferences
.set("selected_window_tab", "log");
1300 const hideLogTab = function() {
1301 $("logTabColumn").addClass("invisible");
1302 MochaUI
.Desktop
.resizePanels();
1303 window
.qBittorrent
.Log
&& window
.qBittorrent
.Log
.unload();
1306 const addSearchPanel = function() {
1318 contentURL
: "views/search.html",
1320 js
: ["scripts/search.js"],
1322 isSearchPanelLoaded
= true;
1326 column
: "searchTabColumn",
1331 const addRssPanel = function() {
1343 contentURL
: "views/rss.html",
1345 column
: "rssTabColumn",
1350 const addLogPanel = function() {
1362 contentURL
: "views/log.html",
1364 css
: ["css/vanillaSelectBox.css"],
1365 js
: ["scripts/lib/vanillaSelectBox.js"],
1367 isLogPanelLoaded
= true;
1370 tabsURL
: "views/logTabs.html",
1371 tabsOnload: function() {
1372 MochaUI
.initializeTabs("panelTabs");
1374 $("logMessageLink").addEventListener("click", (e
) => {
1375 window
.qBittorrent
.Log
.setCurrentTab("main");
1378 $("logPeerLink").addEventListener("click", (e
) => {
1379 window
.qBittorrent
.Log
.setCurrentTab("peer");
1384 column
: "logTabColumn",
1389 const handleDownloadParam = function() {
1390 // Extract torrent URL from download param in WebUI URL hash
1391 const downloadHash
= "#download=";
1392 if (location
.hash
.indexOf(downloadHash
) !== 0)
1395 const url
= decodeURIComponent(location
.hash
.substring(downloadHash
.length
));
1396 // Remove the processed hash from the URL
1397 history
.replaceState("", document
.title
, (location
.pathname
+ location
.search
));
1398 showDownloadPage([url
]);
1412 contentURL
: "views/transferlist.html",
1413 onContentLoaded: function() {
1414 handleDownloadParam();
1417 column
: "mainColumn",
1418 onResize
: window
.qBittorrent
.Misc
.createDebounceHandler(500, (e
) => {
1423 let prop_h
= LocalPreferences
.get("properties_height_rel");
1424 if (prop_h
!== null)
1425 prop_h
= prop_h
.toFloat() * Window
.getSize().y
;
1427 prop_h
= Window
.getSize().y
/ 2.0;
1429 id
: "propertiesPanel",
1437 contentURL
: "views/properties.html",
1439 js
: ["scripts/prop-general.js", "scripts/prop-trackers.js", "scripts/prop-peers.js", "scripts/prop-webseeds.js", "scripts/prop-files.js"],
1440 onload: function() {
1441 updatePropertiesPanel = function() {
1442 switch (LocalPreferences
.get("selected_properties_tab")) {
1443 case "propGeneralLink":
1444 window
.qBittorrent
.PropGeneral
.updateData();
1446 case "propTrackersLink":
1447 window
.qBittorrent
.PropTrackers
.updateData();
1449 case "propPeersLink":
1450 window
.qBittorrent
.PropPeers
.updateData();
1452 case "propWebSeedsLink":
1453 window
.qBittorrent
.PropWebseeds
.updateData();
1455 case "propFilesLink":
1456 window
.qBittorrent
.PropFiles
.updateData();
1462 tabsURL
: "views/propertiesToolbar.html",
1463 tabsOnload: function() {}, // must be included, otherwise panel won't load properly
1464 onContentLoaded: function() {
1465 this.panelHeaderCollapseBoxEl
.classList
.add("invisible");
1467 const togglePropertiesPanel
= () => {
1468 this.collapseToggleEl
.click();
1469 LocalPreferences
.set("properties_panel_collapsed", this.isCollapsed
.toString());
1472 const selectTab
= (tabID
) => {
1473 const isAlreadySelected
= this.panelHeaderEl
.getElementById(tabID
).classList
.contains("selected");
1474 if (!isAlreadySelected
) {
1475 for (const tab
of this.panelHeaderEl
.getElementById("propertiesTabs").children
)
1476 tab
.classList
.toggle("selected", tab
.id
=== tabID
);
1478 const tabContentID
= tabID
.replace("Link", "");
1479 for (const tabContent
of this.contentEl
.children
)
1480 tabContent
.classList
.toggle("invisible", tabContent
.id
!== tabContentID
);
1482 LocalPreferences
.set("selected_properties_tab", tabID
);
1485 if (isAlreadySelected
|| this.isCollapsed
)
1486 togglePropertiesPanel();
1489 const lastUsedTab
= LocalPreferences
.get("selected_properties_tab", "propGeneralLink");
1490 selectTab(lastUsedTab
);
1492 const startCollapsed
= LocalPreferences
.get("properties_panel_collapsed", "false") === "true";
1494 togglePropertiesPanel();
1496 this.panelHeaderContentEl
.addEventListener("click", (e
) => {
1497 const selectedTab
= e
.target
.closest("li");
1501 selectTab(selectedTab
.id
);
1502 updatePropertiesPanel();
1504 const showFilesFilter
= (selectedTab
.id
=== "propFilesLink") && !this.isCollapsed
;
1505 document
.getElementById("torrentFilesFilterToolbar").classList
.toggle("invisible", !showFilesFilter
);
1508 column
: "mainColumn",
1512 // listen for changes to torrentsFilterInput
1513 let torrentsFilterInputTimer
= -1;
1514 $("torrentsFilterInput").addEventListener("input", () => {
1515 clearTimeout(torrentsFilterInputTimer
);
1516 torrentsFilterInputTimer
= setTimeout(() => {
1517 torrentsFilterInputTimer
= -1;
1518 torrentsTable
.updateTable();
1519 }, window
.qBittorrent
.Misc
.FILTER_INPUT_DELAY
);
1522 document
.getElementById("torrentsFilterToolbar").addEventListener("change", (e
) => { torrentsTable
.updateTable(); });
1524 $("transfersTabLink").addEventListener("click", () => { showTransfersTab(); });
1525 $("searchTabLink").addEventListener("click", () => { showSearchTab(); });
1526 $("rssTabLink").addEventListener("click", () => { showRssTab(); });
1527 $("logTabLink").addEventListener("click", () => { showLogTab(); });
1530 const registerDragAndDrop
= () => {
1531 $("desktop").addEventListener("dragover", (ev
) => {
1532 if (ev
.preventDefault
)
1533 ev
.preventDefault();
1536 $("desktop").addEventListener("dragenter", (ev
) => {
1537 if (ev
.preventDefault
)
1538 ev
.preventDefault();
1541 $("desktop").addEventListener("drop", (ev
) => {
1542 if (ev
.preventDefault
)
1543 ev
.preventDefault();
1545 const droppedFiles
= ev
.dataTransfer
.files
;
1547 if (droppedFiles
.length
> 0) {
1548 // dropped files or folders
1550 // can't handle folder due to cannot put the filelist (from dropped folder)
1551 // to <input> `files` field
1552 for (const item
of ev
.dataTransfer
.items
) {
1553 if (item
.webkitGetAsEntry().isDirectory
)
1557 const id
= "uploadPage";
1558 new MochaUI
.Window({
1560 icon
: "images/qbittorrent-tray.svg",
1561 title
: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
1562 loadMethod
: "iframe",
1563 contentURL
: new URI("upload.html").toString(),
1564 addClass
: "windowFrame", // fixes iframe scrolling on iOS Safari
1568 paddingHorizontal
: 0,
1569 width
: loadWindowWidth(id
, 500),
1570 height
: loadWindowHeight(id
, 460),
1571 onResize
: window
.qBittorrent
.Misc
.createDebounceHandler(500, (e
) => {
1574 onContentLoaded
: () => {
1575 const fileInput
= $(`${id}_iframe`).contentDocument
.getElementById("fileselect");
1576 fileInput
.files
= droppedFiles
;
1581 const droppedText
= ev
.dataTransfer
.getData("text");
1582 if (droppedText
.length
> 0) {
1585 const urls
= droppedText
.split("\n")
1586 .map((str
) => str
.trim())
1588 const lowercaseStr
= str
.toLowerCase();
1589 return lowercaseStr
.startsWith("http:")
1590 || lowercaseStr
.startsWith("https:")
1591 || lowercaseStr
.startsWith("magnet:")
1592 || ((str
.length
=== 40) && !(/[^0-9A-F]/i.test(str
))) // v1 hex-encoded SHA-1 info-hash
1593 || ((str
.length
=== 32) && !(/[^2-7A-Z]/i.test(str
))); // v1 Base32 encoded SHA-1 info-hash
1596 if (urls
.length
<= 0)
1599 const id
= "downloadPage";
1600 const contentURI
= new URI("download.html").setData("urls", urls
.map(encodeURIComponent
).join("|"));
1601 new MochaUI
.Window({
1603 icon
: "images/qbittorrent-tray.svg",
1604 title
: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
1605 loadMethod
: "iframe",
1606 contentURL
: contentURI
.toString(),
1607 addClass
: "windowFrame", // fixes iframe scrolling on iOS Safari
1612 paddingHorizontal
: 0,
1613 width
: loadWindowWidth(id
, 500),
1614 height
: loadWindowHeight(id
, 600),
1615 onResize
: window
.qBittorrent
.Misc
.createDebounceHandler(500, (e
) => {
1622 registerDragAndDrop();
1625 defaultEventType
: "keydown",
1627 "ctrl+a": function(event
) {
1628 if ((event
.target
.nodeName
=== "INPUT") || (event
.target
.nodeName
=== "TEXTAREA"))
1630 if (event
.target
.isContentEditable
)
1632 torrentsTable
.selectAll();
1633 event
.preventDefault();
1635 "delete": function(event
) {
1636 if ((event
.target
.nodeName
=== "INPUT") || (event
.target
.nodeName
=== "TEXTAREA"))
1638 if (event
.target
.isContentEditable
)
1641 event
.preventDefault();
1643 "shift+delete": (event
) => {
1644 if ((event
.target
.nodeName
=== "INPUT") || (event
.target
.nodeName
=== "TEXTAREA"))
1646 if (event
.target
.isContentEditable
)
1649 event
.preventDefault();
1655 window
.addEventListener("load", () => {
1656 // fetch various data and store it in memory
1657 window
.qBittorrent
.Cache
.buildInfo
.init();
1658 window
.qBittorrent
.Cache
.preferences
.init();
1659 window
.qBittorrent
.Cache
.qbtVersion
.init();
1661 // switch to previously used tab
1662 const previouslyUsedTab
= LocalPreferences
.get("selected_window_tab", "transfers");
1663 switch (previouslyUsedTab
) {
1665 if (window
.qBittorrent
.Client
.isShowSearchEngine())
1666 $("searchTabLink").click();
1669 if (window
.qBittorrent
.Client
.isShowRssReader())
1670 $("rssTabLink").click();
1673 if (window
.qBittorrent
.Client
.isShowLogViewer())
1674 $("logTabLink").click();
1677 $("transfersTabLink").click();
1680 console
.error(`Unexpected 'selected_window_tab' value: ${previouslyUsedTab}`);
1681 $("transfersTabLink").click();