3 * Copyright (C) 2024 Mike Tzou (Chocobo1)
4 * Copyright (c) 2008 Ishan Arora <ishan@qbittorrent.org>,
5 * Christophe Dumez <chris@qbittorrent.org>
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28 window.qBittorrent ??= {};
29 window.qBittorrent.Client ??= (() => {
30 const exports = () => {
32 closeWindows: closeWindows,
34 getSyncMainDataInterval: getSyncMainDataInterval,
38 showSearchEngine: showSearchEngine,
39 showRssReader: showRssReader,
40 showLogViewer: showLogViewer,
41 isShowSearchEngine: isShowSearchEngine,
42 isShowRssReader: isShowRssReader,
43 isShowLogViewer: isShowLogViewer
47 const closeWindows = function() {
51 const genHash = function(string) {
53 // https://stackoverflow.com/a/8831937
54 // https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0
56 for (let i = 0; i < string.length; ++i)
57 hash = ((Math.imul(hash, 31) + string.charCodeAt(i)) | 0);
61 const getSyncMainDataInterval = function() {
62 return customSyncMainDataInterval ? customSyncMainDataInterval : serverSyncMainDataInterval;
66 const isStopped = () => {
74 const mainTitle = () => {
75 const emDash = "\u2014";
76 const qbtVersion = window.qBittorrent.Cache.qbtVersion.get();
77 const suffix = window.qBittorrent.Cache.preferences.get()["app_instance_name"] || "";
78 const title = `qBittorrent ${qbtVersion} QBT_TR(WebUI)QBT_TR[CONTEXT=OptionsDialog]`
79 + ((suffix.length > 0) ? ` ${emDash} ${suffix}` : "");
83 let showingSearchEngine = false;
84 let showingRssReader = false;
85 let showingLogViewer = false;
87 const showSearchEngine = function(bool) {
88 showingSearchEngine = bool;
90 const showRssReader = function(bool) {
91 showingRssReader = bool;
93 const showLogViewer = function(bool) {
94 showingLogViewer = bool;
96 const isShowSearchEngine = function() {
97 return showingSearchEngine;
99 const isShowRssReader = function() {
100 return showingRssReader;
102 const isShowLogViewer = function() {
103 return showingLogViewer;
108 Object.freeze(window.qBittorrent.Client);
110 // TODO: move global functions/variables into some namespace/scope
112 this.torrentsTable = new window.qBittorrent.DynamicTable.TorrentsTable();
114 let updatePropertiesPanel = function() {};
116 this.updateMainData = function() {};
117 let alternativeSpeedLimits = false;
118 let queueing_enabled = true;
119 let serverSyncMainDataInterval = 1500;
120 let customSyncMainDataInterval = null;
121 let useSubcategories = true;
122 const useAutoHideZeroStatusFilters = LocalPreferences.get("hide_zero_status_filters", "false") === "true";
124 /* Categories filter */
125 const CATEGORIES_ALL = 1;
126 const CATEGORIES_UNCATEGORIZED = 2;
128 const category_list = new Map();
130 let selected_category = Number(LocalPreferences.get("selected_category", CATEGORIES_ALL));
131 let setCategoryFilter = function() {};
135 const TAGS_UNTAGGED = 2;
137 const tagList = new Map();
139 let selectedTag = Number(LocalPreferences.get("selected_tag", TAGS_ALL));
140 let setTagFilter = function() {};
142 /* Trackers filter */
143 const TRACKERS_ALL = 1;
144 const TRACKERS_TRACKERLESS = 2;
146 /** @type Map<number, {host: string, trackerTorrentMap: Map<string, string[]>}> **/
147 const trackerList = new Map();
149 let selectedTracker = LocalPreferences.get("selected_tracker", TRACKERS_ALL);
150 let setTrackerFilter = function() {};
153 let selected_filter = LocalPreferences.get("selected_filter", "all");
154 let setFilter = function() {};
155 let toggleFilterDisplay = function() {};
157 window.addEventListener("DOMContentLoaded", () => {
158 let isSearchPanelLoaded = false;
159 let isLogPanelLoaded = false;
161 const saveColumnSizes = function() {
162 const filters_width = $("Filters").getSize().x;
163 LocalPreferences.set("filters_width", filters_width);
164 const properties_height_rel = $("propertiesPanel").getSize().y / Window.getSize().y;
165 LocalPreferences.set("properties_height_rel", properties_height_rel);
168 window.addEventListener("resize", () => {
169 // only save sizes if the columns are visible
170 if (!$("mainColumn").hasClass("invisible"))
171 saveColumnSizes.delay(200); // Resizing might takes some time.
174 /* MochaUI.Desktop = new MochaUI.Desktop();
175 MochaUI.Desktop.desktop.style.background = "#fff";
176 MochaUI.Desktop.desktop.style.visibility = "visible"; */
177 MochaUI.Desktop.initialize();
179 const buildTransfersTab = function() {
180 const filt_w = Number(LocalPreferences.get("filters_width", 120));
184 onResize: saveColumnSizes,
186 resizeLimit: [1, 300]
194 const buildSearchTab = function() {
196 id: "searchTabColumn",
202 $("searchTabColumn").addClass("invisible");
205 const buildRssTab = function() {
213 $("rssTabColumn").addClass("invisible");
216 const buildLogTab = function() {
224 $("logTabColumn").addClass("invisible");
231 MochaUI.initializeTabs("mainWindowTabsList");
233 setCategoryFilter = function(hash) {
234 selected_category = hash;
235 LocalPreferences.set("selected_category", selected_category);
236 highlightSelectedCategory();
237 if (typeof torrentsTable.tableBody !== "undefined")
241 setTagFilter = function(hash) {
243 LocalPreferences.set("selected_tag", selectedTag);
244 highlightSelectedTag();
245 if (torrentsTable.tableBody !== undefined)
249 setTrackerFilter = function(hash) {
250 selectedTracker = hash.toString();
251 LocalPreferences.set("selected_tracker", selectedTracker);
252 highlightSelectedTracker();
253 if (torrentsTable.tableBody !== undefined)
257 setFilter = function(f) {
258 // Visually Select the right filter
259 $("all_filter").removeClass("selectedFilter");
260 $("downloading_filter").removeClass("selectedFilter");
261 $("seeding_filter").removeClass("selectedFilter");
262 $("completed_filter").removeClass("selectedFilter");
263 $("stopped_filter").removeClass("selectedFilter");
264 $("running_filter").removeClass("selectedFilter");
265 $("active_filter").removeClass("selectedFilter");
266 $("inactive_filter").removeClass("selectedFilter");
267 $("stalled_filter").removeClass("selectedFilter");
268 $("stalled_uploading_filter").removeClass("selectedFilter");
269 $("stalled_downloading_filter").removeClass("selectedFilter");
270 $("checking_filter").removeClass("selectedFilter");
271 $("moving_filter").removeClass("selectedFilter");
272 $("errored_filter").removeClass("selectedFilter");
273 $(f + "_filter").addClass("selectedFilter");
275 LocalPreferences.set("selected_filter", f);
277 if (typeof torrentsTable.tableBody !== "undefined")
281 toggleFilterDisplay = function(filter) {
282 const element = filter + "FilterList";
283 LocalPreferences.set("filter_" + filter + "_collapsed", !$(element).hasClass("invisible"));
284 $(element).toggleClass("invisible");
285 const parent = $(element).getParent(".filterWrapper");
286 const toggleIcon = $(parent).getChildren(".filterTitle img");
288 toggleIcon[0].toggleClass("rotate");
302 contentURL: "views/filters.html",
303 onContentLoaded: function() {
304 setFilter(selected_filter);
306 column: "filtersColumn",
311 // Show Top Toolbar is enabled by default
312 let showTopToolbar = LocalPreferences.get("show_top_toolbar", "true") === "true";
313 if (!showTopToolbar) {
314 $("showTopToolbarLink").firstChild.style.opacity = "0";
315 $("mochaToolbar").addClass("invisible");
318 // Show Status Bar is enabled by default
319 let showStatusBar = LocalPreferences.get("show_status_bar", "true") === "true";
320 if (!showStatusBar) {
321 $("showStatusBarLink").firstChild.style.opacity = "0";
322 $("desktopFooterWrapper").addClass("invisible");
325 // Show Filters Sidebar is enabled by default
326 let showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
327 if (!showFiltersSidebar) {
328 $("showFiltersSidebarLink").firstChild.style.opacity = "0";
329 $("filtersColumn").addClass("invisible");
330 $("filtersColumn_handle").addClass("invisible");
333 let speedInTitle = LocalPreferences.get("speed_in_browser_title_bar") === "true";
335 $("speedInBrowserTitleBarLink").firstChild.style.opacity = "0";
337 // After showing/hiding the toolbar + status bar
338 window.qBittorrent.Client.showSearchEngine(LocalPreferences.get("show_search_engine") !== "false");
339 window.qBittorrent.Client.showRssReader(LocalPreferences.get("show_rss_reader") !== "false");
340 window.qBittorrent.Client.showLogViewer(LocalPreferences.get("show_log_viewer") === "true");
342 // After Show Top Toolbar
343 MochaUI.Desktop.setDesktopSize();
345 let syncMainDataLastResponseId = 0;
346 const serverState = {};
348 const removeTorrentFromCategoryList = function(hash) {
353 category_list.forEach((category) => {
354 const deleteResult = category.torrents.delete(hash);
355 removed ||= deleteResult;
361 const addTorrentToCategoryList = function(torrent) {
362 const category = torrent["category"];
363 if (typeof category === "undefined")
366 const hash = torrent["hash"];
367 if (category.length === 0) { // Empty category
368 removeTorrentFromCategoryList(hash);
372 const categoryHash = window.qBittorrent.Client.genHash(category);
373 if (!category_list.has(categoryHash)) { // This should not happen
374 category_list.set(categoryHash, {
380 const torrents = category_list.get(categoryHash).torrents;
381 if (!torrents.has(hash)) {
382 removeTorrentFromCategoryList(hash);
389 const removeTorrentFromTagList = function(hash) {
394 tagList.forEach((tag) => {
395 const deleteResult = tag.torrents.delete(hash);
396 removed ||= deleteResult;
402 const addTorrentToTagList = function(torrent) {
403 if (torrent["tags"] === undefined) // Tags haven't changed
406 const hash = torrent["hash"];
407 removeTorrentFromTagList(hash);
409 if (torrent["tags"].length === 0) // No tags
412 const tags = torrent["tags"].split(",");
414 for (let i = 0; i < tags.length; ++i) {
415 const tagHash = window.qBittorrent.Client.genHash(tags[i].trim());
416 if (!tagList.has(tagHash)) { // This should not happen
417 tagList.set(tagHash, {
423 const torrents = tagList.get(tagHash).torrents;
424 if (!torrents.has(hash)) {
432 const updateFilter = function(filter, filterTitle) {
433 const filterEl = document.getElementById(`${filter}_filter`);
434 const filterTorrentCount = torrentsTable.getFilteredTorrentsNumber(filter, CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
435 if (useAutoHideZeroStatusFilters) {
436 const hideFilter = (filterTorrentCount === 0) && (filter !== "all");
437 if (filterEl.classList.toggle("invisible", hideFilter))
440 filterEl.firstElementChild.lastChild.nodeValue = filterTitle.replace("%1", filterTorrentCount);
443 const updateFiltersList = function() {
444 updateFilter("all", "QBT_TR(All (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
445 updateFilter("downloading", "QBT_TR(Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
446 updateFilter("seeding", "QBT_TR(Seeding (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
447 updateFilter("completed", "QBT_TR(Completed (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
448 updateFilter("running", "QBT_TR(Running (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
449 updateFilter("stopped", "QBT_TR(Stopped (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
450 updateFilter("active", "QBT_TR(Active (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
451 updateFilter("inactive", "QBT_TR(Inactive (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
452 updateFilter("stalled", "QBT_TR(Stalled (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
453 updateFilter("stalled_uploading", "QBT_TR(Stalled Uploading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
454 updateFilter("stalled_downloading", "QBT_TR(Stalled Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
455 updateFilter("checking", "QBT_TR(Checking (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
456 updateFilter("moving", "QBT_TR(Moving (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
457 updateFilter("errored", "QBT_TR(Errored (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
460 const updateCategoryList = function() {
461 const categoryList = $("categoryFilterList");
464 categoryList.getChildren().each(c => c.destroy());
466 const create_link = function(hash, text, count) {
467 let display_name = text;
469 if (useSubcategories) {
470 const category_path = text.split("/");
471 display_name = category_path[category_path.length - 1];
472 margin_left = (category_path.length - 1) * 20;
475 const span = document.createElement("span");
476 span.classList.add("link");
478 span.style.marginLeft = `${margin_left}px`;
479 span.textContent = `${display_name} (${count})`;
480 span.addEventListener("click", (event) => {
481 event.preventDefault();
482 setCategoryFilter(hash);
485 const img = document.createElement("img");
486 img.src = "images/view-categories.svg";
489 const listItem = document.createElement("li");
491 listItem.appendChild(span);
493 window.qBittorrent.Filters.categoriesFilterContextMenu.addTarget(listItem);
497 const all = torrentsTable.getRowIds().length;
498 let uncategorized = 0;
499 for (const key in torrentsTable.rows) {
500 if (!Object.hasOwn(torrentsTable.rows, key))
503 const row = torrentsTable.rows[key];
504 if (row["full_data"].category.length === 0)
507 categoryList.appendChild(create_link(CATEGORIES_ALL, "QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]", all));
508 categoryList.appendChild(create_link(CATEGORIES_UNCATEGORIZED, "QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]", uncategorized));
510 const sortedCategories = [];
511 category_list.forEach((category, hash) => sortedCategories.push({
512 categoryName: category.name,
514 categoryCount: category.torrents.size
516 sortedCategories.sort((left, right) => {
517 const leftSegments = left.categoryName.split("/");
518 const rightSegments = right.categoryName.split("/");
520 for (let i = 0, iMax = Math.min(leftSegments.length, rightSegments.length); i < iMax; ++i) {
521 const compareResult = window.qBittorrent.Misc.naturalSortCollator.compare(
522 leftSegments[i], rightSegments[i]);
523 if (compareResult !== 0)
524 return compareResult;
527 return leftSegments.length - rightSegments.length;
530 for (let i = 0; i < sortedCategories.length; ++i) {
531 const { categoryName, categoryHash } = sortedCategories[i];
532 let { categoryCount } = sortedCategories[i];
534 if (useSubcategories) {
535 for (let j = (i + 1);
536 ((j < sortedCategories.length) && sortedCategories[j].categoryName.startsWith(categoryName + "/")); ++j)
537 categoryCount += sortedCategories[j].categoryCount;
540 categoryList.appendChild(create_link(categoryHash, categoryName, categoryCount));
543 highlightSelectedCategory();
546 const highlightSelectedCategory = function() {
547 const categoryList = $("categoryFilterList");
550 const children = categoryList.childNodes;
551 for (let i = 0; i < children.length; ++i) {
552 if (Number(children[i].id) === selected_category)
553 children[i].className = "selectedFilter";
555 children[i].className = "";
559 const updateTagList = function() {
560 const tagFilterList = $("tagFilterList");
561 if (tagFilterList === null)
564 tagFilterList.getChildren().each(c => c.destroy());
566 const createLink = function(hash, text, count) {
567 const span = document.createElement("span");
568 span.classList.add("link");
570 span.textContent = `${text} (${count})`;
571 span.addEventListener("click", (event) => {
572 event.preventDefault();
576 const img = document.createElement("img");
577 img.src = "images/tags.svg";
580 const listItem = document.createElement("li");
582 listItem.appendChild(span);
584 window.qBittorrent.Filters.tagsFilterContextMenu.addTarget(listItem);
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 highlightSelectedTag();
611 const highlightSelectedTag = function() {
612 const tagFilterList = $("tagFilterList");
616 const children = tagFilterList.childNodes;
617 for (let i = 0; i < children.length; ++i)
618 children[i].className = (Number(children[i].id) === selectedTag) ? "selectedFilter" : "";
621 // getHost emulate the GUI version `QString getHost(const QString &url)`
622 const getHost = function(url) {
623 // We want the hostname.
624 // If failed to parse the domain, original input should be returned
626 if (!/^(?:https?|udp):/i.test(url))
630 // hack: URL can not get hostname from udp protocol
631 const parsedUrl = new URL(url.replace(/^udp:/i, "https:"));
632 // host: "example.com:8443"
633 // hostname: "example.com"
634 const host = parsedUrl.hostname;
645 const updateTrackerList = function() {
646 const trackerFilterList = $("trackerFilterList");
647 if (trackerFilterList === null)
650 trackerFilterList.getChildren().each(c => c.destroy());
652 const createLink = function(hash, text, count) {
653 const span = document.createElement("span");
654 span.classList.add("link");
656 span.textContent = text.replace("%1", count);
657 span.addEventListener("click", (event) => {
658 event.preventDefault();
659 setTrackerFilter(hash);
662 const img = document.createElement("img");
663 img.src = "images/trackers.svg";
666 const listItem = document.createElement("li");
668 listItem.appendChild(span);
670 window.qBittorrent.Filters.trackersFilterContextMenu.addTarget(listItem);
674 const torrentsCount = torrentsTable.getRowIds().length;
675 trackerFilterList.appendChild(createLink(TRACKERS_ALL, "QBT_TR(All (%1))QBT_TR[CONTEXT=TrackerFiltersList]", torrentsCount));
676 let trackerlessTorrentsCount = 0;
677 for (const key in torrentsTable.rows) {
678 if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]["full_data"].trackers_count === 0))
679 trackerlessTorrentsCount += 1;
681 trackerFilterList.appendChild(createLink(TRACKERS_TRACKERLESS, "QBT_TR(Trackerless (%1))QBT_TR[CONTEXT=TrackerFiltersList]", trackerlessTorrentsCount));
683 // Sort trackers by hostname
684 const sortedList = [];
685 trackerList.forEach(({ host, trackerTorrentMap }, hash) => {
686 const uniqueTorrents = new Set();
687 for (const torrents of trackerTorrentMap.values()) {
688 for (const torrent of torrents)
689 uniqueTorrents.add(torrent);
695 trackerCount: uniqueTorrents.size,
698 sortedList.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.trackerHost, right.trackerHost));
699 for (const { trackerHost, trackerHash, trackerCount } of sortedList)
700 trackerFilterList.appendChild(createLink(trackerHash, (trackerHost + " (%1)"), trackerCount));
702 highlightSelectedTracker();
705 const highlightSelectedTracker = function() {
706 const trackerFilterList = $("trackerFilterList");
707 if (!trackerFilterList)
710 const children = trackerFilterList.childNodes;
711 for (const child of children)
712 child.className = (child.id === selectedTracker) ? "selectedFilter" : "";
715 const setupCopyEventHandler = (function() {
720 clipboardEvent.destroy();
722 clipboardEvent = new ClipboardJS(".copyToClipboard", {
723 text: function(trigger) {
724 switch (trigger.id) {
727 case "copyInfohash1":
728 return copyInfohashFN(1);
729 case "copyInfohash2":
730 return copyInfohashFN(2);
731 case "copyMagnetLink":
732 return copyMagnetLinkFN();
736 return copyCommentFN();
745 let syncMainDataTimeoutID = -1;
746 let syncRequestInProgress = false;
747 const syncMainData = function() {
748 const url = new URI("api/v2/sync/maindata");
749 url.setData("rid", syncMainDataLastResponseId);
750 const request = new Request.JSON({
754 onFailure: function() {
755 const errorDiv = $("error_div");
757 errorDiv.textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]";
758 syncRequestInProgress = false;
761 onSuccess: function(response) {
762 $("error_div").textContent = "";
764 clearTimeout(torrentsFilterInputTimer);
765 torrentsFilterInputTimer = -1;
767 let torrentsTableSelectedRows;
768 let update_categories = false;
769 let updateTags = false;
770 let updateTrackers = false;
771 const full_update = (response["full_update"] === true);
773 torrentsTableSelectedRows = torrentsTable.selectedRowsIds();
774 update_categories = true;
776 updateTrackers = true;
777 torrentsTable.clear();
778 category_list.clear();
783 syncMainDataLastResponseId = response["rid"];
784 if (response["categories"]) {
785 for (const key in response["categories"]) {
786 if (!Object.hasOwn(response["categories"], key))
789 const responseCategory = response["categories"][key];
790 const categoryHash = window.qBittorrent.Client.genHash(key);
791 const category = category_list.get(categoryHash);
792 if (category !== undefined) {
793 // only the save path can change for existing categories
794 category.savePath = responseCategory.savePath;
797 category_list.set(categoryHash, {
798 name: responseCategory.name,
799 savePath: responseCategory.savePath,
804 update_categories = true;
806 if (response["categories_removed"]) {
807 response["categories_removed"].each((category) => {
808 const categoryHash = window.qBittorrent.Client.genHash(category);
809 category_list.delete(categoryHash);
811 update_categories = true;
813 if (response["tags"]) {
814 for (const tag of response["tags"]) {
815 const tagHash = window.qBittorrent.Client.genHash(tag);
816 if (!tagList.has(tagHash)) {
817 tagList.set(tagHash, {
825 if (response["tags_removed"]) {
826 for (let i = 0; i < response["tags_removed"].length; ++i) {
827 const tagHash = window.qBittorrent.Client.genHash(response["tags_removed"][i]);
828 tagList.delete(tagHash);
832 if (response["trackers"]) {
833 for (const [tracker, torrents] of Object.entries(response["trackers"])) {
834 const host = getHost(tracker);
835 const hash = window.qBittorrent.Client.genHash(host);
837 let trackerListItem = trackerList.get(hash);
838 if (trackerListItem === undefined) {
839 trackerListItem = { host: host, trackerTorrentMap: new Map() };
840 trackerList.set(hash, trackerListItem);
843 trackerListItem.trackerTorrentMap.set(tracker, [...torrents]);
845 updateTrackers = true;
847 if (response["trackers_removed"]) {
848 for (let i = 0; i < response["trackers_removed"].length; ++i) {
849 const tracker = response["trackers_removed"][i];
850 const hash = window.qBittorrent.Client.genHash(getHost(tracker));
851 const trackerListEntry = trackerList.get(hash);
852 if (trackerListEntry)
853 trackerListEntry.trackerTorrentMap.delete(tracker);
855 updateTrackers = true;
857 if (response["torrents"]) {
858 let updateTorrentList = false;
859 for (const key in response["torrents"]) {
860 if (!Object.hasOwn(response["torrents"], key))
863 response["torrents"][key]["hash"] = key;
864 response["torrents"][key]["rowId"] = key;
865 if (response["torrents"][key]["state"])
866 response["torrents"][key]["status"] = response["torrents"][key]["state"];
867 torrentsTable.updateRowData(response["torrents"][key]);
868 if (addTorrentToCategoryList(response["torrents"][key]))
869 update_categories = true;
870 if (addTorrentToTagList(response["torrents"][key]))
872 if (response["torrents"][key]["name"])
873 updateTorrentList = true;
876 if (updateTorrentList)
877 setupCopyEventHandler();
879 if (response["torrents_removed"]) {
880 response["torrents_removed"].each((hash) => {
881 torrentsTable.removeRow(hash);
882 removeTorrentFromCategoryList(hash);
883 update_categories = true; // Always to update All category
884 removeTorrentFromTagList(hash);
885 updateTags = true; // Always to update All tag
888 torrentsTable.updateTable(full_update);
889 if (response["server_state"]) {
890 const tmp = response["server_state"];
891 for (const k in tmp) {
892 if (!Object.hasOwn(tmp, k))
894 serverState[k] = tmp[k];
896 processServerState();
899 if (update_categories) {
900 updateCategoryList();
901 window.qBittorrent.TransferList.contextMenu.updateCategoriesSubMenu(category_list);
905 window.qBittorrent.TransferList.contextMenu.updateTagsSubMenu(tagList);
911 // re-select previously selected rows
912 torrentsTable.reselectRows(torrentsTableSelectedRows);
914 syncRequestInProgress = false;
915 syncData(window.qBittorrent.Client.getSyncMainDataInterval());
918 syncRequestInProgress = true;
922 updateMainData = function() {
923 torrentsTable.updateTable();
927 const syncData = function(delay) {
928 if (syncRequestInProgress)
931 clearTimeout(syncMainDataTimeoutID);
932 syncMainDataTimeoutID = -1;
934 if (window.qBittorrent.Client.isStopped())
937 syncMainDataTimeoutID = syncMainData.delay(delay);
940 const processServerState = function() {
941 let transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true);
942 if (serverState.dl_rate_limit > 0)
943 transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_rate_limit, true) + "]";
944 transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_data, false) + ")";
945 $("DlInfos").textContent = transfer_info;
946 transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true);
947 if (serverState.up_rate_limit > 0)
948 transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.up_rate_limit, true) + "]";
949 transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.up_info_data, false) + ")";
950 $("UpInfos").textContent = transfer_info;
952 document.title = (speedInTitle
953 ? (`QBT_TR([D: %1, U: %2])QBT_TR[CONTEXT=MainWindow] `
954 .replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true))
955 .replace("%2", window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true)))
957 + window.qBittorrent.Client.mainTitle();
959 $("freeSpaceOnDisk").textContent = "QBT_TR(Free space: %1)QBT_TR[CONTEXT=HttpServer]".replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.free_space_on_disk));
960 $("DHTNodes").textContent = "QBT_TR(DHT: %1 nodes)QBT_TR[CONTEXT=StatusBar]".replace("%1", serverState.dht_nodes);
963 if (document.getElementById("statisticsContent")) {
964 $("AlltimeDL").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.alltime_dl, false);
965 $("AlltimeUL").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.alltime_ul, false);
966 $("TotalWastedSession").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_wasted_session, false);
967 $("GlobalRatio").textContent = serverState.global_ratio;
968 $("TotalPeerConnections").textContent = serverState.total_peer_connections;
969 $("ReadCacheHits").textContent = serverState.read_cache_hits + "%";
970 $("TotalBuffersSize").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_buffers_size, false);
971 $("WriteCacheOverload").textContent = serverState.write_cache_overload + "%";
972 $("ReadCacheOverload").textContent = serverState.read_cache_overload + "%";
973 $("QueuedIOJobs").textContent = serverState.queued_io_jobs;
974 $("AverageTimeInQueue").textContent = serverState.average_time_queue + " ms";
975 $("TotalQueuedSize").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_queued_size, false);
978 switch (serverState.connection_status) {
980 $("connectionStatus").src = "images/connected.svg";
981 $("connectionStatus").alt = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
982 $("connectionStatus").title = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
985 $("connectionStatus").src = "images/firewalled.svg";
986 $("connectionStatus").alt = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
987 $("connectionStatus").title = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
990 $("connectionStatus").src = "images/disconnected.svg";
991 $("connectionStatus").alt = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
992 $("connectionStatus").title = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
996 if (queueing_enabled !== serverState.queueing) {
997 queueing_enabled = serverState.queueing;
998 torrentsTable.columns["priority"].force_hide = !queueing_enabled;
999 torrentsTable.updateColumn("priority");
1000 if (queueing_enabled) {
1001 $("topQueuePosItem").removeClass("invisible");
1002 $("increaseQueuePosItem").removeClass("invisible");
1003 $("decreaseQueuePosItem").removeClass("invisible");
1004 $("bottomQueuePosItem").removeClass("invisible");
1005 $("queueingButtons").removeClass("invisible");
1006 $("queueingMenuItems").removeClass("invisible");
1009 $("topQueuePosItem").addClass("invisible");
1010 $("increaseQueuePosItem").addClass("invisible");
1011 $("decreaseQueuePosItem").addClass("invisible");
1012 $("bottomQueuePosItem").addClass("invisible");
1013 $("queueingButtons").addClass("invisible");
1014 $("queueingMenuItems").addClass("invisible");
1018 if (alternativeSpeedLimits !== serverState.use_alt_speed_limits) {
1019 alternativeSpeedLimits = serverState.use_alt_speed_limits;
1020 updateAltSpeedIcon(alternativeSpeedLimits);
1023 if (useSubcategories !== serverState.use_subcategories) {
1024 useSubcategories = serverState.use_subcategories;
1025 updateCategoryList();
1028 serverSyncMainDataInterval = Math.max(serverState.refresh_interval, 500);
1031 const updateAltSpeedIcon = function(enabled) {
1033 $("alternativeSpeedLimits").src = "images/slow.svg";
1034 $("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
1035 $("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
1038 $("alternativeSpeedLimits").src = "images/slow_off.svg";
1039 $("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
1040 $("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
1044 $("alternativeSpeedLimits").addEventListener("click", () => {
1045 // Change icon immediately to give some feedback
1046 updateAltSpeedIcon(!alternativeSpeedLimits);
1049 url: "api/v2/transfer/toggleSpeedLimitsMode",
1051 onComplete: function() {
1052 alternativeSpeedLimits = !alternativeSpeedLimits;
1055 onFailure: function() {
1056 // Restore icon in case of failure
1057 updateAltSpeedIcon(alternativeSpeedLimits);
1062 $("DlInfos").addEventListener("click", globalDownloadLimitFN);
1063 $("UpInfos").addEventListener("click", globalUploadLimitFN);
1065 $("showTopToolbarLink").addEventListener("click", (e) => {
1066 showTopToolbar = !showTopToolbar;
1067 LocalPreferences.set("show_top_toolbar", showTopToolbar.toString());
1068 if (showTopToolbar) {
1069 $("showTopToolbarLink").firstChild.style.opacity = "1";
1070 $("mochaToolbar").removeClass("invisible");
1073 $("showTopToolbarLink").firstChild.style.opacity = "0";
1074 $("mochaToolbar").addClass("invisible");
1076 MochaUI.Desktop.setDesktopSize();
1079 $("showStatusBarLink").addEventListener("click", (e) => {
1080 showStatusBar = !showStatusBar;
1081 LocalPreferences.set("show_status_bar", showStatusBar.toString());
1082 if (showStatusBar) {
1083 $("showStatusBarLink").firstChild.style.opacity = "1";
1084 $("desktopFooterWrapper").removeClass("invisible");
1087 $("showStatusBarLink").firstChild.style.opacity = "0";
1088 $("desktopFooterWrapper").addClass("invisible");
1090 MochaUI.Desktop.setDesktopSize();
1093 const registerMagnetHandler = function() {
1094 if (typeof navigator.registerProtocolHandler !== "function") {
1095 if (window.location.protocol !== "https:")
1096 alert("QBT_TR(To use this feature, the WebUI needs to be accessed over HTTPS)QBT_TR[CONTEXT=MainWindow]");
1098 alert("QBT_TR(Your browser does not support this feature)QBT_TR[CONTEXT=MainWindow]");
1102 const hashString = location.hash ? location.hash.replace(/^#/, "") : "";
1103 const hashParams = new URLSearchParams(hashString);
1104 hashParams.set("download", "");
1106 const templateHashString = hashParams.toString().replace("download=", "download=%s");
1107 const templateUrl = location.origin + location.pathname
1108 + location.search + "#" + templateHashString;
1110 navigator.registerProtocolHandler("magnet", templateUrl,
1111 "qBittorrent WebUI magnet handler");
1113 $("registerMagnetHandlerLink").addEventListener("click", (e) => {
1114 registerMagnetHandler();
1117 $("showFiltersSidebarLink").addEventListener("click", (e) => {
1118 showFiltersSidebar = !showFiltersSidebar;
1119 LocalPreferences.set("show_filters_sidebar", showFiltersSidebar.toString());
1120 if (showFiltersSidebar) {
1121 $("showFiltersSidebarLink").firstChild.style.opacity = "1";
1122 $("filtersColumn").removeClass("invisible");
1123 $("filtersColumn_handle").removeClass("invisible");
1126 $("showFiltersSidebarLink").firstChild.style.opacity = "0";
1127 $("filtersColumn").addClass("invisible");
1128 $("filtersColumn_handle").addClass("invisible");
1130 MochaUI.Desktop.setDesktopSize();
1133 $("speedInBrowserTitleBarLink").addEventListener("click", (e) => {
1134 speedInTitle = !speedInTitle;
1135 LocalPreferences.set("speed_in_browser_title_bar", speedInTitle.toString());
1137 $("speedInBrowserTitleBarLink").firstChild.style.opacity = "1";
1139 $("speedInBrowserTitleBarLink").firstChild.style.opacity = "0";
1140 processServerState();
1143 $("showSearchEngineLink").addEventListener("click", (e) => {
1144 window.qBittorrent.Client.showSearchEngine(!window.qBittorrent.Client.isShowSearchEngine());
1145 LocalPreferences.set("show_search_engine", window.qBittorrent.Client.isShowSearchEngine().toString());
1149 $("showRssReaderLink").addEventListener("click", (e) => {
1150 window.qBittorrent.Client.showRssReader(!window.qBittorrent.Client.isShowRssReader());
1151 LocalPreferences.set("show_rss_reader", window.qBittorrent.Client.isShowRssReader().toString());
1155 $("showLogViewerLink").addEventListener("click", (e) => {
1156 window.qBittorrent.Client.showLogViewer(!window.qBittorrent.Client.isShowLogViewer());
1157 LocalPreferences.set("show_log_viewer", window.qBittorrent.Client.isShowLogViewer().toString());
1161 const updateTabDisplay = function() {
1162 if (window.qBittorrent.Client.isShowRssReader()) {
1163 $("showRssReaderLink").firstChild.style.opacity = "1";
1164 $("mainWindowTabs").removeClass("invisible");
1165 $("rssTabLink").removeClass("invisible");
1166 if (!MochaUI.Panels.instances.RssPanel)
1170 $("showRssReaderLink").firstChild.style.opacity = "0";
1171 $("rssTabLink").addClass("invisible");
1172 if ($("rssTabLink").hasClass("selected"))
1173 $("transfersTabLink").click();
1176 if (window.qBittorrent.Client.isShowSearchEngine()) {
1177 $("showSearchEngineLink").firstChild.style.opacity = "1";
1178 $("mainWindowTabs").removeClass("invisible");
1179 $("searchTabLink").removeClass("invisible");
1180 if (!MochaUI.Panels.instances.SearchPanel)
1184 $("showSearchEngineLink").firstChild.style.opacity = "0";
1185 $("searchTabLink").addClass("invisible");
1186 if ($("searchTabLink").hasClass("selected"))
1187 $("transfersTabLink").click();
1190 if (window.qBittorrent.Client.isShowLogViewer()) {
1191 $("showLogViewerLink").firstChild.style.opacity = "1";
1192 $("mainWindowTabs").removeClass("invisible");
1193 $("logTabLink").removeClass("invisible");
1194 if (!MochaUI.Panels.instances.LogPanel)
1198 $("showLogViewerLink").firstChild.style.opacity = "0";
1199 $("logTabLink").addClass("invisible");
1200 if ($("logTabLink").hasClass("selected"))
1201 $("transfersTabLink").click();
1205 if (!window.qBittorrent.Client.isShowRssReader() && !window.qBittorrent.Client.isShowSearchEngine() && !window.qBittorrent.Client.isShowLogViewer())
1206 $("mainWindowTabs").addClass("invisible");
1209 $("StatisticsLink").addEventListener("click", StatisticsLinkFN);
1213 const showTransfersTab = function() {
1214 const showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
1215 if (showFiltersSidebar) {
1216 $("filtersColumn").removeClass("invisible");
1217 $("filtersColumn_handle").removeClass("invisible");
1219 $("mainColumn").removeClass("invisible");
1220 $("torrentsFilterToolbar").removeClass("invisible");
1222 customSyncMainDataInterval = null;
1229 LocalPreferences.set("selected_window_tab", "transfers");
1232 const hideTransfersTab = function() {
1233 $("filtersColumn").addClass("invisible");
1234 $("filtersColumn_handle").addClass("invisible");
1235 $("mainColumn").addClass("invisible");
1236 $("torrentsFilterToolbar").addClass("invisible");
1237 MochaUI.Desktop.resizePanels();
1240 const showSearchTab = (function() {
1241 let searchTabInitialized = false;
1244 // we must wait until the panel is fully loaded before proceeding.
1245 // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1246 // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1247 if (!isSearchPanelLoaded) {
1254 if (!searchTabInitialized) {
1255 window.qBittorrent.Search.init();
1256 searchTabInitialized = true;
1259 $("searchTabColumn").removeClass("invisible");
1260 customSyncMainDataInterval = 30000;
1265 LocalPreferences.set("selected_window_tab", "search");
1269 const hideSearchTab = function() {
1270 $("searchTabColumn").addClass("invisible");
1271 MochaUI.Desktop.resizePanels();
1274 const showRssTab = (function() {
1275 let rssTabInitialized = false;
1278 if (!rssTabInitialized) {
1279 window.qBittorrent.Rss.init();
1280 rssTabInitialized = true;
1283 window.qBittorrent.Rss.load();
1286 $("rssTabColumn").removeClass("invisible");
1287 customSyncMainDataInterval = 30000;
1292 LocalPreferences.set("selected_window_tab", "rss");
1296 const hideRssTab = function() {
1297 $("rssTabColumn").addClass("invisible");
1298 window.qBittorrent.Rss && window.qBittorrent.Rss.unload();
1299 MochaUI.Desktop.resizePanels();
1302 const showLogTab = (function() {
1303 let logTabInitialized = false;
1306 // we must wait until the panel is fully loaded before proceeding.
1307 // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1308 // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1309 if (!isLogPanelLoaded) {
1316 if (!logTabInitialized) {
1317 window.qBittorrent.Log.init();
1318 logTabInitialized = true;
1321 window.qBittorrent.Log.load();
1324 $("logTabColumn").removeClass("invisible");
1325 customSyncMainDataInterval = 30000;
1330 LocalPreferences.set("selected_window_tab", "log");
1334 const hideLogTab = function() {
1335 $("logTabColumn").addClass("invisible");
1336 MochaUI.Desktop.resizePanels();
1337 window.qBittorrent.Log && window.qBittorrent.Log.unload();
1340 const addSearchPanel = function() {
1352 contentURL: "views/search.html",
1354 js: ["scripts/search.js"],
1356 isSearchPanelLoaded = true;
1360 column: "searchTabColumn",
1365 const addRssPanel = function() {
1377 contentURL: "views/rss.html",
1379 column: "rssTabColumn",
1384 const addLogPanel = function() {
1396 contentURL: "views/log.html",
1398 css: ["css/vanillaSelectBox.css"],
1399 js: ["scripts/lib/vanillaSelectBox.js"],
1401 isLogPanelLoaded = true;
1404 tabsURL: "views/logTabs.html",
1405 tabsOnload: function() {
1406 MochaUI.initializeTabs("panelTabs");
1408 $("logMessageLink").addEventListener("click", (e) => {
1409 window.qBittorrent.Log.setCurrentTab("main");
1412 $("logPeerLink").addEventListener("click", (e) => {
1413 window.qBittorrent.Log.setCurrentTab("peer");
1418 column: "logTabColumn",
1423 const handleDownloadParam = function() {
1424 // Extract torrent URL from download param in WebUI URL hash
1425 const downloadHash = "#download=";
1426 if (location.hash.indexOf(downloadHash) !== 0)
1429 const url = decodeURIComponent(location.hash.substring(downloadHash.length));
1430 // Remove the processed hash from the URL
1431 history.replaceState("", document.title, (location.pathname + location.search));
1432 showDownloadPage([url]);
1446 contentURL: "views/transferlist.html",
1447 onContentLoaded: function() {
1448 handleDownloadParam();
1451 column: "mainColumn",
1452 onResize: saveColumnSizes,
1455 let prop_h = LocalPreferences.get("properties_height_rel");
1456 if (prop_h !== null)
1457 prop_h = prop_h.toFloat() * Window.getSize().y;
1459 prop_h = Window.getSize().y / 2.0;
1461 id: "propertiesPanel",
1470 contentURL: "views/properties.html",
1472 css: ["css/Tabs.css", "css/dynamicTable.css"],
1473 js: ["scripts/prop-general.js", "scripts/prop-trackers.js", "scripts/prop-peers.js", "scripts/prop-webseeds.js", "scripts/prop-files.js"],
1475 tabsURL: "views/propertiesToolbar.html",
1476 tabsOnload: function() {
1477 MochaUI.initializeTabs("propertiesTabs");
1479 updatePropertiesPanel = function() {
1480 if (!$("prop_general").hasClass("invisible")) {
1481 if (window.qBittorrent.PropGeneral !== undefined)
1482 window.qBittorrent.PropGeneral.updateData();
1484 else if (!$("prop_trackers").hasClass("invisible")) {
1485 if (window.qBittorrent.PropTrackers !== undefined)
1486 window.qBittorrent.PropTrackers.updateData();
1488 else if (!$("prop_peers").hasClass("invisible")) {
1489 if (window.qBittorrent.PropPeers !== undefined)
1490 window.qBittorrent.PropPeers.updateData();
1492 else if (!$("prop_webseeds").hasClass("invisible")) {
1493 if (window.qBittorrent.PropWebseeds !== undefined)
1494 window.qBittorrent.PropWebseeds.updateData();
1496 else if (!$("prop_files").hasClass("invisible")) {
1497 if (window.qBittorrent.PropFiles !== undefined)
1498 window.qBittorrent.PropFiles.updateData();
1502 $("PropGeneralLink").addEventListener("click", function(e) {
1503 $$(".propertiesTabContent").addClass("invisible");
1504 $("prop_general").removeClass("invisible");
1506 updatePropertiesPanel();
1507 LocalPreferences.set("selected_tab", this.id);
1510 $("PropTrackersLink").addEventListener("click", function(e) {
1511 $$(".propertiesTabContent").addClass("invisible");
1512 $("prop_trackers").removeClass("invisible");
1514 updatePropertiesPanel();
1515 LocalPreferences.set("selected_tab", this.id);
1518 $("PropPeersLink").addEventListener("click", function(e) {
1519 $$(".propertiesTabContent").addClass("invisible");
1520 $("prop_peers").removeClass("invisible");
1522 updatePropertiesPanel();
1523 LocalPreferences.set("selected_tab", this.id);
1526 $("PropWebSeedsLink").addEventListener("click", function(e) {
1527 $$(".propertiesTabContent").addClass("invisible");
1528 $("prop_webseeds").removeClass("invisible");
1530 updatePropertiesPanel();
1531 LocalPreferences.set("selected_tab", this.id);
1534 $("PropFilesLink").addEventListener("click", function(e) {
1535 $$(".propertiesTabContent").addClass("invisible");
1536 $("prop_files").removeClass("invisible");
1538 updatePropertiesPanel();
1539 LocalPreferences.set("selected_tab", this.id);
1542 $("propertiesPanel_collapseToggle").addEventListener("click", (e) => {
1543 updatePropertiesPanel();
1546 column: "mainColumn",
1550 const showFilesFilter = function() {
1551 $("torrentFilesFilterToolbar").removeClass("invisible");
1554 const hideFilesFilter = function() {
1555 $("torrentFilesFilterToolbar").addClass("invisible");
1558 // listen for changes to torrentsFilterInput
1559 let torrentsFilterInputTimer = -1;
1560 $("torrentsFilterInput").addEventListener("input", () => {
1561 clearTimeout(torrentsFilterInputTimer);
1562 torrentsFilterInputTimer = setTimeout(() => {
1563 torrentsFilterInputTimer = -1;
1564 torrentsTable.updateTable();
1565 }, window.qBittorrent.Misc.FILTER_INPUT_DELAY);
1568 document.getElementById("torrentsFilterToolbar").addEventListener("change", (e) => { torrentsTable.updateTable(); });
1570 $("transfersTabLink").addEventListener("click", showTransfersTab);
1571 $("searchTabLink").addEventListener("click", showSearchTab);
1572 $("rssTabLink").addEventListener("click", showRssTab);
1573 $("logTabLink").addEventListener("click", showLogTab);
1576 const registerDragAndDrop = () => {
1577 $("desktop").addEventListener("dragover", (ev) => {
1578 if (ev.preventDefault)
1579 ev.preventDefault();
1582 $("desktop").addEventListener("dragenter", (ev) => {
1583 if (ev.preventDefault)
1584 ev.preventDefault();
1587 $("desktop").addEventListener("drop", (ev) => {
1588 if (ev.preventDefault)
1589 ev.preventDefault();
1591 const droppedFiles = ev.dataTransfer.files;
1593 if (droppedFiles.length > 0) {
1594 // dropped files or folders
1596 // can't handle folder due to cannot put the filelist (from dropped folder)
1597 // to <input> `files` field
1598 for (const item of ev.dataTransfer.items) {
1599 if (item.webkitGetAsEntry().isDirectory)
1603 const id = "uploadPage";
1604 new MochaUI.Window({
1606 icon: "images/qbittorrent-tray.svg",
1607 title: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
1608 loadMethod: "iframe",
1609 contentURL: new URI("upload.html").toString(),
1610 addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
1614 paddingHorizontal: 0,
1615 width: loadWindowWidth(id, 500),
1616 height: loadWindowHeight(id, 460),
1620 onContentLoaded: () => {
1621 const fileInput = $(`${id}_iframe`).contentDocument.getElementById("fileselect");
1622 fileInput.files = droppedFiles;
1627 const droppedText = ev.dataTransfer.getData("text");
1628 if (droppedText.length > 0) {
1631 const urls = droppedText.split("\n")
1632 .map((str) => str.trim())
1634 const lowercaseStr = str.toLowerCase();
1635 return lowercaseStr.startsWith("http:")
1636 || lowercaseStr.startsWith("https:")
1637 || lowercaseStr.startsWith("magnet:")
1638 || ((str.length === 40) && !(/[^0-9A-F]/i.test(str))) // v1 hex-encoded SHA-1 info-hash
1639 || ((str.length === 32) && !(/[^2-7A-Z]/i.test(str))); // v1 Base32 encoded SHA-1 info-hash
1642 if (urls.length <= 0)
1645 const id = "downloadPage";
1646 const contentURI = new URI("download.html").setData("urls", urls.map(encodeURIComponent).join("|"));
1647 new MochaUI.Window({
1649 icon: "images/qbittorrent-tray.svg",
1650 title: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
1651 loadMethod: "iframe",
1652 contentURL: contentURI.toString(),
1653 addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
1658 paddingHorizontal: 0,
1659 width: loadWindowWidth(id, 500),
1660 height: loadWindowHeight(id, 600),
1668 registerDragAndDrop();
1671 defaultEventType: "keydown",
1673 "ctrl+a": function(event) {
1674 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1676 if (event.target.isContentEditable)
1678 torrentsTable.selectAll();
1679 event.preventDefault();
1681 "delete": function(event) {
1682 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1684 if (event.target.isContentEditable)
1687 event.preventDefault();
1689 "shift+delete": (event) => {
1690 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1692 if (event.target.isContentEditable)
1695 event.preventDefault();
1701 window.addEventListener("load", () => {
1702 // fetch various data and store it in memory
1703 window.qBittorrent.Cache.buildInfo.init();
1704 window.qBittorrent.Cache.preferences.init();
1705 window.qBittorrent.Cache.qbtVersion.init();
1707 // switch to previously used tab
1708 const previouslyUsedTab = LocalPreferences.get("selected_window_tab", "transfers");
1709 switch (previouslyUsedTab) {
1711 if (window.qBittorrent.Client.isShowSearchEngine())
1712 $("searchTabLink").click();
1715 if (window.qBittorrent.Client.isShowRssReader())
1716 $("rssTabLink").click();
1719 if (window.qBittorrent.Client.isShowLogViewer())
1720 $("logTabLink").click();
1723 $("transfersTabLink").click();
1726 console.error(`Unexpected 'selected_window_tab' value: ${previouslyUsedTab}`);
1727 $("transfersTabLink").click();