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();