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 if (window.qBittorrent === undefined) {
29 window.qBittorrent = {};
32 window.qBittorrent.Client = (() => {
33 const exports = () => {
35 closeWindows: closeWindows,
37 getSyncMainDataInterval: getSyncMainDataInterval,
41 showSearchEngine: showSearchEngine,
42 showRssReader: showRssReader,
43 showLogViewer: showLogViewer,
44 isShowSearchEngine: isShowSearchEngine,
45 isShowRssReader: isShowRssReader,
46 isShowLogViewer: isShowLogViewer
50 const closeWindows = function() {
54 const genHash = function(string) {
56 // https://stackoverflow.com/a/8831937
57 // https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0
59 for (let i = 0; i < string.length; ++i)
60 hash = ((Math.imul(hash, 31) + string.charCodeAt(i)) | 0);
64 const getSyncMainDataInterval = function() {
65 return customSyncMainDataInterval ? customSyncMainDataInterval : serverSyncMainDataInterval;
69 const isStopped = () => {
77 const mainTitle = () => {
78 const emDash = "\u2014";
79 const qbtVersion = window.qBittorrent.Cache.qbtVersion.get();
80 const suffix = window.qBittorrent.Cache.preferences.get()["app_instance_name"] || "";
81 const title = `qBittorrent ${qbtVersion} QBT_TR(WebUI)QBT_TR[CONTEXT=OptionsDialog]`
82 + ((suffix.length > 0) ? ` ${emDash} ${suffix}` : "");
86 let showingSearchEngine = false;
87 let showingRssReader = false;
88 let showingLogViewer = false;
90 const showSearchEngine = function(bool) {
91 showingSearchEngine = bool;
93 const showRssReader = function(bool) {
94 showingRssReader = bool;
96 const showLogViewer = function(bool) {
97 showingLogViewer = bool;
99 const isShowSearchEngine = function() {
100 return showingSearchEngine;
102 const isShowRssReader = function() {
103 return showingRssReader;
105 const isShowLogViewer = function() {
106 return showingLogViewer;
111 Object.freeze(window.qBittorrent.Client);
113 // TODO: move global functions/variables into some namespace/scope
115 this.torrentsTable = new window.qBittorrent.DynamicTable.TorrentsTable();
117 let updatePropertiesPanel = function() {};
119 this.updateMainData = function() {};
120 let alternativeSpeedLimits = false;
121 let queueing_enabled = true;
122 let serverSyncMainDataInterval = 1500;
123 let customSyncMainDataInterval = null;
124 let useSubcategories = true;
126 /* Categories filter */
127 const CATEGORIES_ALL = 1;
128 const CATEGORIES_UNCATEGORIZED = 2;
130 const category_list = new Map();
132 let selected_category = Number(LocalPreferences.get("selected_category", CATEGORIES_ALL));
133 let setCategoryFilter = function() {};
137 const TAGS_UNTAGGED = 2;
139 const tagList = new Map();
141 let selectedTag = Number(LocalPreferences.get("selected_tag", TAGS_ALL));
142 let setTagFilter = function() {};
144 /* Trackers filter */
145 const TRACKERS_ALL = 1;
146 const TRACKERS_TRACKERLESS = 2;
148 /** @type Map<number, {host: string, trackerTorrentMap: Map<string, string[]>}> **/
149 const trackerList = new Map();
151 let selectedTracker = LocalPreferences.get("selected_tracker", TRACKERS_ALL);
152 let setTrackerFilter = function() {};
155 let selected_filter = LocalPreferences.get("selected_filter", "all");
156 let setFilter = function() {};
157 let toggleFilterDisplay = function() {};
159 window.addEventListener("DOMContentLoaded", () => {
160 let isSearchPanelLoaded = false;
161 let isLogPanelLoaded = false;
163 const saveColumnSizes = function() {
164 const filters_width = $("Filters").getSize().x;
165 LocalPreferences.set("filters_width", filters_width);
166 const properties_height_rel = $("propertiesPanel").getSize().y / Window.getSize().y;
167 LocalPreferences.set("properties_height_rel", properties_height_rel);
170 window.addEvent("resize", () => {
171 // only save sizes if the columns are visible
172 if (!$("mainColumn").hasClass("invisible"))
173 saveColumnSizes.delay(200); // Resizing might takes some time.
176 /*MochaUI.Desktop = new MochaUI.Desktop();
177 MochaUI.Desktop.desktop.setStyles({
178 'background': '#fff',
179 'visibility': 'visible'
181 MochaUI.Desktop.initialize();
183 const buildTransfersTab = function() {
184 const filt_w = Number(LocalPreferences.get("filters_width", 120));
188 onResize: saveColumnSizes,
190 resizeLimit: [1, 300]
198 const buildSearchTab = function() {
200 id: "searchTabColumn",
206 $("searchTabColumn").addClass("invisible");
209 const buildRssTab = function() {
217 $("rssTabColumn").addClass("invisible");
220 const buildLogTab = function() {
228 $("logTabColumn").addClass("invisible");
235 MochaUI.initializeTabs("mainWindowTabsList");
237 setCategoryFilter = function(hash) {
238 selected_category = hash;
239 LocalPreferences.set("selected_category", selected_category);
240 highlightSelectedCategory();
241 if (typeof torrentsTable.tableBody !== "undefined")
245 setTagFilter = function(hash) {
247 LocalPreferences.set("selected_tag", selectedTag);
248 highlightSelectedTag();
249 if (torrentsTable.tableBody !== undefined)
253 setTrackerFilter = function(hash) {
254 selectedTracker = hash.toString();
255 LocalPreferences.set("selected_tracker", selectedTracker);
256 highlightSelectedTracker();
257 if (torrentsTable.tableBody !== undefined)
261 setFilter = function(f) {
262 // Visually Select the right filter
263 $("all_filter").removeClass("selectedFilter");
264 $("downloading_filter").removeClass("selectedFilter");
265 $("seeding_filter").removeClass("selectedFilter");
266 $("completed_filter").removeClass("selectedFilter");
267 $("stopped_filter").removeClass("selectedFilter");
268 $("running_filter").removeClass("selectedFilter");
269 $("active_filter").removeClass("selectedFilter");
270 $("inactive_filter").removeClass("selectedFilter");
271 $("stalled_filter").removeClass("selectedFilter");
272 $("stalled_uploading_filter").removeClass("selectedFilter");
273 $("stalled_downloading_filter").removeClass("selectedFilter");
274 $("checking_filter").removeClass("selectedFilter");
275 $("moving_filter").removeClass("selectedFilter");
276 $("errored_filter").removeClass("selectedFilter");
277 $(f + "_filter").addClass("selectedFilter");
279 LocalPreferences.set("selected_filter", f);
281 if (typeof torrentsTable.tableBody !== "undefined")
285 toggleFilterDisplay = function(filter) {
286 const element = filter + "FilterList";
287 LocalPreferences.set("filter_" + filter + "_collapsed", !$(element).hasClass("invisible"));
288 $(element).toggleClass("invisible");
289 const parent = $(element).getParent(".filterWrapper");
290 const toggleIcon = $(parent).getChildren(".filterTitle img");
292 toggleIcon[0].toggleClass("rotate");
306 contentURL: "views/filters.html",
307 onContentLoaded: function() {
308 setFilter(selected_filter);
310 column: "filtersColumn",
315 // Show Top Toolbar is enabled by default
316 let showTopToolbar = LocalPreferences.get("show_top_toolbar", "true") === "true";
317 if (!showTopToolbar) {
318 $("showTopToolbarLink").firstChild.style.opacity = "0";
319 $("mochaToolbar").addClass("invisible");
322 // Show Status Bar is enabled by default
323 let showStatusBar = LocalPreferences.get("show_status_bar", "true") === "true";
324 if (!showStatusBar) {
325 $("showStatusBarLink").firstChild.style.opacity = "0";
326 $("desktopFooterWrapper").addClass("invisible");
329 // Show Filters Sidebar is enabled by default
330 let showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
331 if (!showFiltersSidebar) {
332 $("showFiltersSidebarLink").firstChild.style.opacity = "0";
333 $("filtersColumn").addClass("invisible");
334 $("filtersColumn_handle").addClass("invisible");
337 let speedInTitle = LocalPreferences.get("speed_in_browser_title_bar") === "true";
339 $("speedInBrowserTitleBarLink").firstChild.style.opacity = "0";
341 // After showing/hiding the toolbar + status bar
342 window.qBittorrent.Client.showSearchEngine(LocalPreferences.get("show_search_engine") !== "false");
343 window.qBittorrent.Client.showRssReader(LocalPreferences.get("show_rss_reader") !== "false");
344 window.qBittorrent.Client.showLogViewer(LocalPreferences.get("show_log_viewer") === "true");
346 // After Show Top Toolbar
347 MochaUI.Desktop.setDesktopSize();
349 let syncMainDataLastResponseId = 0;
350 const serverState = {};
352 const removeTorrentFromCategoryList = function(hash) {
357 category_list.forEach((category) => {
358 const deleteResult = category.torrents.delete(hash);
359 removed ||= deleteResult;
365 const addTorrentToCategoryList = function(torrent) {
366 const category = torrent["category"];
367 if (typeof category === "undefined")
370 const hash = torrent["hash"];
371 if (category.length === 0) { // Empty category
372 removeTorrentFromCategoryList(hash);
376 const categoryHash = window.qBittorrent.Client.genHash(category);
377 if (!category_list.has(categoryHash)) { // This should not happen
378 category_list.set(categoryHash, {
384 const torrents = category_list.get(categoryHash).torrents;
385 if (!torrents.has(hash)) {
386 removeTorrentFromCategoryList(hash);
393 const removeTorrentFromTagList = function(hash) {
398 tagList.forEach((tag) => {
399 const deleteResult = tag.torrents.delete(hash);
400 removed ||= deleteResult;
406 const addTorrentToTagList = function(torrent) {
407 if (torrent["tags"] === undefined) // Tags haven't changed
410 const hash = torrent["hash"];
411 removeTorrentFromTagList(hash);
413 if (torrent["tags"].length === 0) // No tags
416 const tags = torrent["tags"].split(",");
418 for (let i = 0; i < tags.length; ++i) {
419 const tagHash = window.qBittorrent.Client.genHash(tags[i].trim());
420 if (!tagList.has(tagHash)) { // This should not happen
421 tagList.set(tagHash, {
427 const torrents = tagList.get(tagHash).torrents;
428 if (!torrents.has(hash)) {
436 const updateFilter = function(filter, filterTitle) {
437 $(filter + "_filter").firstChild.childNodes[1].nodeValue = filterTitle.replace("%1", torrentsTable.getFilteredTorrentsNumber(filter, CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL));
440 const updateFiltersList = function() {
441 updateFilter("all", "QBT_TR(All (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
442 updateFilter("downloading", "QBT_TR(Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
443 updateFilter("seeding", "QBT_TR(Seeding (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
444 updateFilter("completed", "QBT_TR(Completed (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
445 updateFilter("running", "QBT_TR(Running (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
446 updateFilter("stopped", "QBT_TR(Stopped (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
447 updateFilter("active", "QBT_TR(Active (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
448 updateFilter("inactive", "QBT_TR(Inactive (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
449 updateFilter("stalled", "QBT_TR(Stalled (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
450 updateFilter("stalled_uploading", "QBT_TR(Stalled Uploading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
451 updateFilter("stalled_downloading", "QBT_TR(Stalled Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
452 updateFilter("checking", "QBT_TR(Checking (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
453 updateFilter("moving", "QBT_TR(Moving (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
454 updateFilter("errored", "QBT_TR(Errored (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
457 const updateCategoryList = function() {
458 const categoryList = $("categoryFilterList");
461 categoryList.getChildren().each(c => c.destroy());
463 const create_link = function(hash, text, count) {
464 let display_name = text;
466 if (useSubcategories) {
467 const category_path = text.split("/");
468 display_name = category_path[category_path.length - 1];
469 margin_left = (category_path.length - 1) * 20;
472 const html = `<span class="link" href="#" style="margin-left: ${margin_left}px;" onclick="setCategoryFilter(${hash}); return false;">`
473 + '<img src="images/view-categories.svg"/>'
474 + window.qBittorrent.Misc.escapeHtml(display_name) + " (" + count + ")" + "</span>";
475 const el = new Element("li", {
479 window.qBittorrent.Filters.categoriesFilterContextMenu.addTarget(el);
483 const all = torrentsTable.getRowIds().length;
484 let uncategorized = 0;
485 for (const key in torrentsTable.rows) {
486 if (!Object.hasOwn(torrentsTable.rows, key))
489 const row = torrentsTable.rows[key];
490 if (row["full_data"].category.length === 0)
493 categoryList.appendChild(create_link(CATEGORIES_ALL, "QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]", all));
494 categoryList.appendChild(create_link(CATEGORIES_UNCATEGORIZED, "QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]", uncategorized));
496 const sortedCategories = [];
497 category_list.forEach((category, hash) => sortedCategories.push({
498 categoryName: category.name,
500 categoryCount: category.torrents.size
502 sortedCategories.sort((left, right) => {
503 const leftSegments = left.categoryName.split("/");
504 const rightSegments = right.categoryName.split("/");
506 for (let i = 0, iMax = Math.min(leftSegments.length, rightSegments.length); i < iMax; ++i) {
507 const compareResult = window.qBittorrent.Misc.naturalSortCollator.compare(
508 leftSegments[i], rightSegments[i]);
509 if (compareResult !== 0)
510 return compareResult;
513 return leftSegments.length - rightSegments.length;
516 for (let i = 0; i < sortedCategories.length; ++i) {
517 const { categoryName, categoryHash } = sortedCategories[i];
518 let { categoryCount } = sortedCategories[i];
520 if (useSubcategories) {
521 for (let j = (i + 1);
522 ((j < sortedCategories.length) && sortedCategories[j].categoryName.startsWith(categoryName + "/")); ++j) {
523 categoryCount += sortedCategories[j].categoryCount;
527 categoryList.appendChild(create_link(categoryHash, categoryName, categoryCount));
530 highlightSelectedCategory();
533 const highlightSelectedCategory = function() {
534 const categoryList = $("categoryFilterList");
537 const children = categoryList.childNodes;
538 for (let i = 0; i < children.length; ++i) {
539 if (Number(children[i].id) === selected_category)
540 children[i].className = "selectedFilter";
542 children[i].className = "";
546 const updateTagList = function() {
547 const tagFilterList = $("tagFilterList");
548 if (tagFilterList === null)
551 tagFilterList.getChildren().each(c => c.destroy());
553 const createLink = function(hash, text, count) {
554 const html = `<span class="link" href="#" onclick="setTagFilter(${hash}); return false;">`
555 + '<img src="images/tags.svg"/>'
556 + window.qBittorrent.Misc.escapeHtml(text) + " (" + count + ")" + "</span>";
557 const el = new Element("li", {
561 window.qBittorrent.Filters.tagsFilterContextMenu.addTarget(el);
565 const torrentsCount = torrentsTable.getRowIds().length;
567 for (const key in torrentsTable.rows) {
568 if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]["full_data"].tags.length === 0))
571 tagFilterList.appendChild(createLink(TAGS_ALL, "QBT_TR(All)QBT_TR[CONTEXT=TagFilterModel]", torrentsCount));
572 tagFilterList.appendChild(createLink(TAGS_UNTAGGED, "QBT_TR(Untagged)QBT_TR[CONTEXT=TagFilterModel]", untagged));
574 const sortedTags = [];
575 tagList.forEach((tag, hash) => sortedTags.push({
578 tagSize: tag.torrents.size
580 sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName));
582 for (const { tagName, tagHash, tagSize } of sortedTags)
583 tagFilterList.appendChild(createLink(tagHash, tagName, tagSize));
585 highlightSelectedTag();
588 const highlightSelectedTag = function() {
589 const tagFilterList = $("tagFilterList");
593 const children = tagFilterList.childNodes;
594 for (let i = 0; i < children.length; ++i)
595 children[i].className = (Number(children[i].id) === selectedTag) ? "selectedFilter" : "";
598 // getHost emulate the GUI version `QString getHost(const QString &url)`
599 const getHost = function(url) {
600 // We want the hostname.
601 // If failed to parse the domain, original input should be returned
603 if (!/^(?:https?|udp):/i.test(url)) {
608 // hack: URL can not get hostname from udp protocol
609 const parsedUrl = new URL(url.replace(/^udp:/i, "https:"));
610 // host: "example.com:8443"
611 // hostname: "example.com"
612 const host = parsedUrl.hostname;
624 const updateTrackerList = function() {
625 const trackerFilterList = $("trackerFilterList");
626 if (trackerFilterList === null)
629 trackerFilterList.getChildren().each(c => c.destroy());
631 const createLink = function(hash, text, count) {
632 const html = '<span class="link" href="#" onclick="setTrackerFilter(' + hash + ');return false;">'
633 + '<img src="images/trackers.svg"/>'
634 + window.qBittorrent.Misc.escapeHtml(text.replace("%1", count)) + "</span>";
635 const el = new Element("li", {
639 window.qBittorrent.Filters.trackersFilterContextMenu.addTarget(el);
643 const torrentsCount = torrentsTable.getRowIds().length;
644 trackerFilterList.appendChild(createLink(TRACKERS_ALL, "QBT_TR(All (%1))QBT_TR[CONTEXT=TrackerFiltersList]", torrentsCount));
645 let trackerlessTorrentsCount = 0;
646 for (const key in torrentsTable.rows) {
647 if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]["full_data"].trackers_count === 0))
648 trackerlessTorrentsCount += 1;
650 trackerFilterList.appendChild(createLink(TRACKERS_TRACKERLESS, "QBT_TR(Trackerless (%1))QBT_TR[CONTEXT=TrackerFiltersList]", trackerlessTorrentsCount));
652 // Sort trackers by hostname
653 const sortedList = [];
654 trackerList.forEach(({ host, trackerTorrentMap }, hash) => {
655 const uniqueTorrents = new Set();
656 for (const torrents of trackerTorrentMap.values()) {
657 for (const torrent of torrents) {
658 uniqueTorrents.add(torrent);
665 trackerCount: uniqueTorrents.size,
668 sortedList.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.trackerHost, right.trackerHost));
669 for (const { trackerHost, trackerHash, trackerCount } of sortedList)
670 trackerFilterList.appendChild(createLink(trackerHash, (trackerHost + " (%1)"), trackerCount));
672 highlightSelectedTracker();
675 const highlightSelectedTracker = function() {
676 const trackerFilterList = $("trackerFilterList");
677 if (!trackerFilterList)
680 const children = trackerFilterList.childNodes;
681 for (const child of children)
682 child.className = (child.id === selectedTracker) ? "selectedFilter" : "";
685 const setupCopyEventHandler = (function() {
690 clipboardEvent.destroy();
692 clipboardEvent = new ClipboardJS(".copyToClipboard", {
693 text: function(trigger) {
694 switch (trigger.id) {
697 case "copyInfohash1":
698 return copyInfohashFN(1);
699 case "copyInfohash2":
700 return copyInfohashFN(2);
701 case "copyMagnetLink":
702 return copyMagnetLinkFN();
706 return copyCommentFN();
715 let syncMainDataTimeoutID;
716 let syncRequestInProgress = false;
717 const syncMainData = function() {
718 const url = new URI("api/v2/sync/maindata");
719 url.setData("rid", syncMainDataLastResponseId);
720 const request = new Request.JSON({
724 onFailure: function() {
725 const errorDiv = $("error_div");
727 errorDiv.set("html", "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]");
728 syncRequestInProgress = false;
731 onSuccess: function(response) {
732 $("error_div").set("html", "");
734 clearTimeout(torrentsFilterInputTimer);
735 torrentsFilterInputTimer = -1;
737 let torrentsTableSelectedRows;
738 let update_categories = false;
739 let updateTags = false;
740 let updateTrackers = false;
741 const full_update = (response["full_update"] === true);
743 torrentsTableSelectedRows = torrentsTable.selectedRowsIds();
744 torrentsTable.clear();
745 category_list.clear();
748 if (response["rid"]) {
749 syncMainDataLastResponseId = response["rid"];
751 if (response["categories"]) {
752 for (const key in response["categories"]) {
753 if (!Object.hasOwn(response["categories"], key))
756 const responseCategory = response["categories"][key];
757 const categoryHash = window.qBittorrent.Client.genHash(key);
758 const category = category_list.get(categoryHash);
759 if (category !== undefined) {
760 // only the save path can change for existing categories
761 category.savePath = responseCategory.savePath;
764 category_list.set(categoryHash, {
765 name: responseCategory.name,
766 savePath: responseCategory.savePath,
771 update_categories = true;
773 if (response["categories_removed"]) {
774 response["categories_removed"].each((category) => {
775 const categoryHash = window.qBittorrent.Client.genHash(category);
776 category_list.delete(categoryHash);
778 update_categories = true;
780 if (response["tags"]) {
781 for (const tag of response["tags"]) {
782 const tagHash = window.qBittorrent.Client.genHash(tag);
783 if (!tagList.has(tagHash)) {
784 tagList.set(tagHash, {
792 if (response["tags_removed"]) {
793 for (let i = 0; i < response["tags_removed"].length; ++i) {
794 const tagHash = window.qBittorrent.Client.genHash(response["tags_removed"][i]);
795 tagList.delete(tagHash);
799 if (response["trackers"]) {
800 for (const [tracker, torrents] of Object.entries(response["trackers"])) {
801 const host = getHost(tracker);
802 const hash = window.qBittorrent.Client.genHash(host);
804 let trackerListItem = trackerList.get(hash);
805 if (trackerListItem === undefined) {
806 trackerListItem = { host: host, trackerTorrentMap: new Map() };
807 trackerList.set(hash, trackerListItem);
810 trackerListItem.trackerTorrentMap.set(tracker, [...torrents]);
812 updateTrackers = true;
814 if (response["trackers_removed"]) {
815 for (let i = 0; i < response["trackers_removed"].length; ++i) {
816 const tracker = response["trackers_removed"][i];
817 const hash = window.qBittorrent.Client.genHash(getHost(tracker));
818 const trackerListEntry = trackerList.get(hash);
819 if (trackerListEntry) {
820 trackerListEntry.trackerTorrentMap.delete(tracker);
823 updateTrackers = true;
825 if (response["torrents"]) {
826 let updateTorrentList = false;
827 for (const key in response["torrents"]) {
828 if (!Object.hasOwn(response["torrents"], key))
831 response["torrents"][key]["hash"] = key;
832 response["torrents"][key]["rowId"] = key;
833 if (response["torrents"][key]["state"])
834 response["torrents"][key]["status"] = response["torrents"][key]["state"];
835 torrentsTable.updateRowData(response["torrents"][key]);
836 if (addTorrentToCategoryList(response["torrents"][key]))
837 update_categories = true;
838 if (addTorrentToTagList(response["torrents"][key]))
840 if (response["torrents"][key]["name"])
841 updateTorrentList = true;
844 if (updateTorrentList)
845 setupCopyEventHandler();
847 if (response["torrents_removed"])
848 response["torrents_removed"].each((hash) => {
849 torrentsTable.removeRow(hash);
850 removeTorrentFromCategoryList(hash);
851 update_categories = true; // Always to update All category
852 removeTorrentFromTagList(hash);
853 updateTags = true; // Always to update All tag
855 torrentsTable.updateTable(full_update);
856 torrentsTable.altRow();
857 if (response["server_state"]) {
858 const tmp = response["server_state"];
859 for (const k in tmp) {
860 if (!Object.hasOwn(tmp, k))
862 serverState[k] = tmp[k];
864 processServerState();
867 if (update_categories) {
868 updateCategoryList();
869 window.qBittorrent.TransferList.contextMenu.updateCategoriesSubMenu(category_list);
873 window.qBittorrent.TransferList.contextMenu.updateTagsSubMenu(tagList);
879 // re-select previously selected rows
880 torrentsTable.reselectRows(torrentsTableSelectedRows);
882 syncRequestInProgress = false;
883 syncData(window.qBittorrent.Client.getSyncMainDataInterval());
886 syncRequestInProgress = true;
890 updateMainData = function() {
891 torrentsTable.updateTable();
895 const syncData = function(delay) {
896 if (syncRequestInProgress)
899 clearTimeout(syncMainDataTimeoutID);
901 if (window.qBittorrent.Client.isStopped())
904 syncMainDataTimeoutID = syncMainData.delay(delay);
907 const processServerState = function() {
908 let transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true);
909 if (serverState.dl_rate_limit > 0)
910 transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_rate_limit, true) + "]";
911 transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_data, false) + ")";
912 $("DlInfos").set("html", transfer_info);
913 transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true);
914 if (serverState.up_rate_limit > 0)
915 transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.up_rate_limit, true) + "]";
916 transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.up_info_data, false) + ")";
917 $("UpInfos").set("html", transfer_info);
919 document.title = (speedInTitle
920 ? (`QBT_TR([D: %1, U: %2])QBT_TR[CONTEXT=MainWindow] `
921 .replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true))
922 .replace("%2", window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true)))
924 + window.qBittorrent.Client.mainTitle();
926 $("freeSpaceOnDisk").set("html", "QBT_TR(Free space: %1)QBT_TR[CONTEXT=HttpServer]".replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.free_space_on_disk)));
927 $("DHTNodes").set("html", "QBT_TR(DHT: %1 nodes)QBT_TR[CONTEXT=StatusBar]".replace("%1", serverState.dht_nodes));
930 if (document.getElementById("statisticsContent")) {
931 $("AlltimeDL").set("html", window.qBittorrent.Misc.friendlyUnit(serverState.alltime_dl, false));
932 $("AlltimeUL").set("html", window.qBittorrent.Misc.friendlyUnit(serverState.alltime_ul, false));
933 $("TotalWastedSession").set("html", window.qBittorrent.Misc.friendlyUnit(serverState.total_wasted_session, false));
934 $("GlobalRatio").set("html", serverState.global_ratio);
935 $("TotalPeerConnections").set("html", serverState.total_peer_connections);
936 $("ReadCacheHits").set("html", serverState.read_cache_hits + "%");
937 $("TotalBuffersSize").set("html", window.qBittorrent.Misc.friendlyUnit(serverState.total_buffers_size, false));
938 $("WriteCacheOverload").set("html", serverState.write_cache_overload + "%");
939 $("ReadCacheOverload").set("html", serverState.read_cache_overload + "%");
940 $("QueuedIOJobs").set("html", serverState.queued_io_jobs);
941 $("AverageTimeInQueue").set("html", serverState.average_time_queue + " ms");
942 $("TotalQueuedSize").set("html", window.qBittorrent.Misc.friendlyUnit(serverState.total_queued_size, false));
945 switch (serverState.connection_status) {
947 $("connectionStatus").src = "images/connected.svg";
948 $("connectionStatus").alt = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
949 $("connectionStatus").title = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
952 $("connectionStatus").src = "images/firewalled.svg";
953 $("connectionStatus").alt = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
954 $("connectionStatus").title = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
957 $("connectionStatus").src = "images/disconnected.svg";
958 $("connectionStatus").alt = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
959 $("connectionStatus").title = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
963 if (queueing_enabled !== serverState.queueing) {
964 queueing_enabled = serverState.queueing;
965 torrentsTable.columns["priority"].force_hide = !queueing_enabled;
966 torrentsTable.updateColumn("priority");
967 if (queueing_enabled) {
968 $("topQueuePosItem").removeClass("invisible");
969 $("increaseQueuePosItem").removeClass("invisible");
970 $("decreaseQueuePosItem").removeClass("invisible");
971 $("bottomQueuePosItem").removeClass("invisible");
972 $("queueingButtons").removeClass("invisible");
973 $("queueingMenuItems").removeClass("invisible");
976 $("topQueuePosItem").addClass("invisible");
977 $("increaseQueuePosItem").addClass("invisible");
978 $("decreaseQueuePosItem").addClass("invisible");
979 $("bottomQueuePosItem").addClass("invisible");
980 $("queueingButtons").addClass("invisible");
981 $("queueingMenuItems").addClass("invisible");
985 if (alternativeSpeedLimits !== serverState.use_alt_speed_limits) {
986 alternativeSpeedLimits = serverState.use_alt_speed_limits;
987 updateAltSpeedIcon(alternativeSpeedLimits);
990 if (useSubcategories !== serverState.use_subcategories) {
991 useSubcategories = serverState.use_subcategories;
992 updateCategoryList();
995 serverSyncMainDataInterval = Math.max(serverState.refresh_interval, 500);
998 const updateAltSpeedIcon = function(enabled) {
1000 $("alternativeSpeedLimits").src = "images/slow.svg";
1001 $("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
1002 $("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
1005 $("alternativeSpeedLimits").src = "images/slow_off.svg";
1006 $("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
1007 $("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
1011 $("alternativeSpeedLimits").addEvent("click", () => {
1012 // Change icon immediately to give some feedback
1013 updateAltSpeedIcon(!alternativeSpeedLimits);
1016 url: "api/v2/transfer/toggleSpeedLimitsMode",
1018 onComplete: function() {
1019 alternativeSpeedLimits = !alternativeSpeedLimits;
1022 onFailure: function() {
1023 // Restore icon in case of failure
1024 updateAltSpeedIcon(alternativeSpeedLimits);
1029 $("DlInfos").addEvent("click", globalDownloadLimitFN);
1030 $("UpInfos").addEvent("click", globalUploadLimitFN);
1032 $("showTopToolbarLink").addEvent("click", (e) => {
1033 showTopToolbar = !showTopToolbar;
1034 LocalPreferences.set("show_top_toolbar", showTopToolbar.toString());
1035 if (showTopToolbar) {
1036 $("showTopToolbarLink").firstChild.style.opacity = "1";
1037 $("mochaToolbar").removeClass("invisible");
1040 $("showTopToolbarLink").firstChild.style.opacity = "0";
1041 $("mochaToolbar").addClass("invisible");
1043 MochaUI.Desktop.setDesktopSize();
1046 $("showStatusBarLink").addEvent("click", (e) => {
1047 showStatusBar = !showStatusBar;
1048 LocalPreferences.set("show_status_bar", showStatusBar.toString());
1049 if (showStatusBar) {
1050 $("showStatusBarLink").firstChild.style.opacity = "1";
1051 $("desktopFooterWrapper").removeClass("invisible");
1054 $("showStatusBarLink").firstChild.style.opacity = "0";
1055 $("desktopFooterWrapper").addClass("invisible");
1057 MochaUI.Desktop.setDesktopSize();
1060 const registerMagnetHandler = function() {
1061 if (typeof navigator.registerProtocolHandler !== "function") {
1062 if (window.location.protocol !== "https:")
1063 alert("QBT_TR(To use this feature, the WebUI needs to be accessed over HTTPS)QBT_TR[CONTEXT=MainWindow]");
1065 alert("QBT_TR(Your browser does not support this feature)QBT_TR[CONTEXT=MainWindow]");
1069 const hashString = location.hash ? location.hash.replace(/^#/, "") : "";
1070 const hashParams = new URLSearchParams(hashString);
1071 hashParams.set("download", "");
1073 const templateHashString = hashParams.toString().replace("download=", "download=%s");
1074 const templateUrl = location.origin + location.pathname
1075 + location.search + "#" + templateHashString;
1077 navigator.registerProtocolHandler("magnet", templateUrl,
1078 "qBittorrent WebUI magnet handler");
1080 $("registerMagnetHandlerLink").addEvent("click", (e) => {
1081 registerMagnetHandler();
1084 $("showFiltersSidebarLink").addEvent("click", (e) => {
1085 showFiltersSidebar = !showFiltersSidebar;
1086 LocalPreferences.set("show_filters_sidebar", showFiltersSidebar.toString());
1087 if (showFiltersSidebar) {
1088 $("showFiltersSidebarLink").firstChild.style.opacity = "1";
1089 $("filtersColumn").removeClass("invisible");
1090 $("filtersColumn_handle").removeClass("invisible");
1093 $("showFiltersSidebarLink").firstChild.style.opacity = "0";
1094 $("filtersColumn").addClass("invisible");
1095 $("filtersColumn_handle").addClass("invisible");
1097 MochaUI.Desktop.setDesktopSize();
1100 $("speedInBrowserTitleBarLink").addEvent("click", (e) => {
1101 speedInTitle = !speedInTitle;
1102 LocalPreferences.set("speed_in_browser_title_bar", speedInTitle.toString());
1104 $("speedInBrowserTitleBarLink").firstChild.style.opacity = "1";
1106 $("speedInBrowserTitleBarLink").firstChild.style.opacity = "0";
1107 processServerState();
1110 $("showSearchEngineLink").addEvent("click", (e) => {
1111 window.qBittorrent.Client.showSearchEngine(!window.qBittorrent.Client.isShowSearchEngine());
1112 LocalPreferences.set("show_search_engine", window.qBittorrent.Client.isShowSearchEngine().toString());
1116 $("showRssReaderLink").addEvent("click", (e) => {
1117 window.qBittorrent.Client.showRssReader(!window.qBittorrent.Client.isShowRssReader());
1118 LocalPreferences.set("show_rss_reader", window.qBittorrent.Client.isShowRssReader().toString());
1122 $("showLogViewerLink").addEvent("click", (e) => {
1123 window.qBittorrent.Client.showLogViewer(!window.qBittorrent.Client.isShowLogViewer());
1124 LocalPreferences.set("show_log_viewer", window.qBittorrent.Client.isShowLogViewer().toString());
1128 const updateTabDisplay = function() {
1129 if (window.qBittorrent.Client.isShowRssReader()) {
1130 $("showRssReaderLink").firstChild.style.opacity = "1";
1131 $("mainWindowTabs").removeClass("invisible");
1132 $("rssTabLink").removeClass("invisible");
1133 if (!MochaUI.Panels.instances.RssPanel)
1137 $("showRssReaderLink").firstChild.style.opacity = "0";
1138 $("rssTabLink").addClass("invisible");
1139 if ($("rssTabLink").hasClass("selected"))
1140 $("transfersTabLink").click();
1143 if (window.qBittorrent.Client.isShowSearchEngine()) {
1144 $("showSearchEngineLink").firstChild.style.opacity = "1";
1145 $("mainWindowTabs").removeClass("invisible");
1146 $("searchTabLink").removeClass("invisible");
1147 if (!MochaUI.Panels.instances.SearchPanel)
1151 $("showSearchEngineLink").firstChild.style.opacity = "0";
1152 $("searchTabLink").addClass("invisible");
1153 if ($("searchTabLink").hasClass("selected"))
1154 $("transfersTabLink").click();
1157 if (window.qBittorrent.Client.isShowLogViewer()) {
1158 $("showLogViewerLink").firstChild.style.opacity = "1";
1159 $("mainWindowTabs").removeClass("invisible");
1160 $("logTabLink").removeClass("invisible");
1161 if (!MochaUI.Panels.instances.LogPanel)
1165 $("showLogViewerLink").firstChild.style.opacity = "0";
1166 $("logTabLink").addClass("invisible");
1167 if ($("logTabLink").hasClass("selected"))
1168 $("transfersTabLink").click();
1172 if (!window.qBittorrent.Client.isShowRssReader() && !window.qBittorrent.Client.isShowSearchEngine() && !window.qBittorrent.Client.isShowLogViewer())
1173 $("mainWindowTabs").addClass("invisible");
1176 $("StatisticsLink").addEvent("click", StatisticsLinkFN);
1180 const showTransfersTab = function() {
1181 const showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
1182 if (showFiltersSidebar) {
1183 $("filtersColumn").removeClass("invisible");
1184 $("filtersColumn_handle").removeClass("invisible");
1186 $("mainColumn").removeClass("invisible");
1187 $("torrentsFilterToolbar").removeClass("invisible");
1189 customSyncMainDataInterval = null;
1196 LocalPreferences.set("selected_tab", "transfers");
1199 const hideTransfersTab = function() {
1200 $("filtersColumn").addClass("invisible");
1201 $("filtersColumn_handle").addClass("invisible");
1202 $("mainColumn").addClass("invisible");
1203 $("torrentsFilterToolbar").addClass("invisible");
1204 MochaUI.Desktop.resizePanels();
1207 const showSearchTab = (function() {
1208 let searchTabInitialized = false;
1211 // we must wait until the panel is fully loaded before proceeding.
1212 // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1213 // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1214 if (!isSearchPanelLoaded) {
1221 if (!searchTabInitialized) {
1222 window.qBittorrent.Search.init();
1223 searchTabInitialized = true;
1226 $("searchTabColumn").removeClass("invisible");
1227 customSyncMainDataInterval = 30000;
1232 LocalPreferences.set("selected_tab", "search");
1236 const hideSearchTab = function() {
1237 $("searchTabColumn").addClass("invisible");
1238 MochaUI.Desktop.resizePanels();
1241 const showRssTab = (function() {
1242 let rssTabInitialized = false;
1245 if (!rssTabInitialized) {
1246 window.qBittorrent.Rss.init();
1247 rssTabInitialized = true;
1250 window.qBittorrent.Rss.load();
1253 $("rssTabColumn").removeClass("invisible");
1254 customSyncMainDataInterval = 30000;
1259 LocalPreferences.set("selected_tab", "rss");
1263 const hideRssTab = function() {
1264 $("rssTabColumn").addClass("invisible");
1265 window.qBittorrent.Rss && window.qBittorrent.Rss.unload();
1266 MochaUI.Desktop.resizePanels();
1269 const showLogTab = (function() {
1270 let logTabInitialized = false;
1273 // we must wait until the panel is fully loaded before proceeding.
1274 // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1275 // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1276 if (!isLogPanelLoaded) {
1283 if (!logTabInitialized) {
1284 window.qBittorrent.Log.init();
1285 logTabInitialized = true;
1288 window.qBittorrent.Log.load();
1291 $("logTabColumn").removeClass("invisible");
1292 customSyncMainDataInterval = 30000;
1297 LocalPreferences.set("selected_tab", "log");
1301 const hideLogTab = function() {
1302 $("logTabColumn").addClass("invisible");
1303 MochaUI.Desktop.resizePanels();
1304 window.qBittorrent.Log && window.qBittorrent.Log.unload();
1307 const addSearchPanel = function() {
1319 contentURL: "views/search.html",
1321 js: ["scripts/search.js"],
1323 isSearchPanelLoaded = true;
1327 column: "searchTabColumn",
1332 const addRssPanel = function() {
1344 contentURL: "views/rss.html",
1346 column: "rssTabColumn",
1351 const addLogPanel = function() {
1363 contentURL: "views/log.html",
1365 css: ["css/vanillaSelectBox.css"],
1366 js: ["scripts/lib/vanillaSelectBox.js"],
1368 isLogPanelLoaded = true;
1371 tabsURL: "views/logTabs.html",
1372 tabsOnload: function() {
1373 MochaUI.initializeTabs("panelTabs");
1375 $("logMessageLink").addEvent("click", (e) => {
1376 window.qBittorrent.Log.setCurrentTab("main");
1379 $("logPeerLink").addEvent("click", (e) => {
1380 window.qBittorrent.Log.setCurrentTab("peer");
1385 column: "logTabColumn",
1390 const handleDownloadParam = function() {
1391 // Extract torrent URL from download param in WebUI URL hash
1392 const downloadHash = "#download=";
1393 if (location.hash.indexOf(downloadHash) !== 0)
1396 const url = decodeURIComponent(location.hash.substring(downloadHash.length));
1397 // Remove the processed hash from the URL
1398 history.replaceState("", document.title, (location.pathname + location.search));
1399 showDownloadPage([url]);
1413 contentURL: "views/transferlist.html",
1414 onContentLoaded: function() {
1415 handleDownloadParam();
1418 column: "mainColumn",
1419 onResize: saveColumnSizes,
1422 let prop_h = LocalPreferences.get("properties_height_rel");
1423 if (prop_h !== null)
1424 prop_h = prop_h.toFloat() * Window.getSize().y;
1426 prop_h = Window.getSize().y / 2.0;
1428 id: "propertiesPanel",
1437 contentURL: "views/properties.html",
1439 css: ["css/Tabs.css", "css/dynamicTable.css"],
1440 js: ["scripts/prop-general.js", "scripts/prop-trackers.js", "scripts/prop-peers.js", "scripts/prop-webseeds.js", "scripts/prop-files.js"],
1442 tabsURL: "views/propertiesToolbar.html",
1443 tabsOnload: function() {
1444 MochaUI.initializeTabs("propertiesTabs");
1446 updatePropertiesPanel = function() {
1447 if (!$("prop_general").hasClass("invisible")) {
1448 if (window.qBittorrent.PropGeneral !== undefined)
1449 window.qBittorrent.PropGeneral.updateData();
1451 else if (!$("prop_trackers").hasClass("invisible")) {
1452 if (window.qBittorrent.PropTrackers !== undefined)
1453 window.qBittorrent.PropTrackers.updateData();
1455 else if (!$("prop_peers").hasClass("invisible")) {
1456 if (window.qBittorrent.PropPeers !== undefined)
1457 window.qBittorrent.PropPeers.updateData();
1459 else if (!$("prop_webseeds").hasClass("invisible")) {
1460 if (window.qBittorrent.PropWebseeds !== undefined)
1461 window.qBittorrent.PropWebseeds.updateData();
1463 else if (!$("prop_files").hasClass("invisible")) {
1464 if (window.qBittorrent.PropFiles !== undefined)
1465 window.qBittorrent.PropFiles.updateData();
1469 $("PropGeneralLink").addEvent("click", function(e) {
1470 $$(".propertiesTabContent").addClass("invisible");
1471 $("prop_general").removeClass("invisible");
1473 updatePropertiesPanel();
1474 LocalPreferences.set("selected_tab", this.id);
1477 $("PropTrackersLink").addEvent("click", function(e) {
1478 $$(".propertiesTabContent").addClass("invisible");
1479 $("prop_trackers").removeClass("invisible");
1481 updatePropertiesPanel();
1482 LocalPreferences.set("selected_tab", this.id);
1485 $("PropPeersLink").addEvent("click", function(e) {
1486 $$(".propertiesTabContent").addClass("invisible");
1487 $("prop_peers").removeClass("invisible");
1489 updatePropertiesPanel();
1490 LocalPreferences.set("selected_tab", this.id);
1493 $("PropWebSeedsLink").addEvent("click", function(e) {
1494 $$(".propertiesTabContent").addClass("invisible");
1495 $("prop_webseeds").removeClass("invisible");
1497 updatePropertiesPanel();
1498 LocalPreferences.set("selected_tab", this.id);
1501 $("PropFilesLink").addEvent("click", function(e) {
1502 $$(".propertiesTabContent").addClass("invisible");
1503 $("prop_files").removeClass("invisible");
1505 updatePropertiesPanel();
1506 LocalPreferences.set("selected_tab", this.id);
1509 $("propertiesPanel_collapseToggle").addEvent("click", (e) => {
1510 updatePropertiesPanel();
1513 column: "mainColumn",
1517 const showFilesFilter = function() {
1518 $("torrentFilesFilterToolbar").removeClass("invisible");
1521 const hideFilesFilter = function() {
1522 $("torrentFilesFilterToolbar").addClass("invisible");
1525 // listen for changes to torrentsFilterInput
1526 let torrentsFilterInputTimer = -1;
1527 $("torrentsFilterInput").addEvent("input", () => {
1528 clearTimeout(torrentsFilterInputTimer);
1529 torrentsFilterInputTimer = setTimeout(() => {
1530 torrentsFilterInputTimer = -1;
1531 torrentsTable.updateTable();
1532 }, window.qBittorrent.Misc.FILTER_INPUT_DELAY);
1534 $("torrentsFilterRegexBox").addEvent("change", () => {
1535 torrentsTable.updateTable();
1538 $("transfersTabLink").addEvent("click", showTransfersTab);
1539 $("searchTabLink").addEvent("click", showSearchTab);
1540 $("rssTabLink").addEvent("click", showRssTab);
1541 $("logTabLink").addEvent("click", showLogTab);
1544 const registerDragAndDrop = () => {
1545 $("desktop").addEventListener("dragover", (ev) => {
1546 if (ev.preventDefault)
1547 ev.preventDefault();
1550 $("desktop").addEventListener("dragenter", (ev) => {
1551 if (ev.preventDefault)
1552 ev.preventDefault();
1555 $("desktop").addEventListener("drop", (ev) => {
1556 if (ev.preventDefault)
1557 ev.preventDefault();
1559 const droppedFiles = ev.dataTransfer.files;
1561 if (droppedFiles.length > 0) {
1562 // dropped files or folders
1564 // can't handle folder due to cannot put the filelist (from dropped folder)
1565 // to <input> `files` field
1566 for (const item of ev.dataTransfer.items) {
1567 if (item.webkitGetAsEntry().isDirectory)
1571 const id = "uploadPage";
1572 new MochaUI.Window({
1574 title: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
1575 loadMethod: "iframe",
1576 contentURL: new URI("upload.html").toString(),
1577 addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
1581 paddingHorizontal: 0,
1582 width: loadWindowWidth(id, 500),
1583 height: loadWindowHeight(id, 460),
1587 onContentLoaded: () => {
1588 const fileInput = $(`${id}_iframe`).contentDocument.getElementById("fileselect");
1589 fileInput.files = droppedFiles;
1594 const droppedText = ev.dataTransfer.getData("text");
1595 if (droppedText.length > 0) {
1598 const urls = droppedText.split("\n")
1599 .map((str) => str.trim())
1601 const lowercaseStr = str.toLowerCase();
1602 return lowercaseStr.startsWith("http:")
1603 || lowercaseStr.startsWith("https:")
1604 || lowercaseStr.startsWith("magnet:")
1605 || ((str.length === 40) && !(/[^0-9A-F]/i.test(str))) // v1 hex-encoded SHA-1 info-hash
1606 || ((str.length === 32) && !(/[^2-7A-Z]/i.test(str))); // v1 Base32 encoded SHA-1 info-hash
1609 if (urls.length <= 0)
1612 const id = "downloadPage";
1613 const contentURI = new URI("download.html").setData("urls", urls.map(encodeURIComponent).join("|"));
1614 new MochaUI.Window({
1616 title: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
1617 loadMethod: "iframe",
1618 contentURL: contentURI.toString(),
1619 addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
1624 paddingHorizontal: 0,
1625 width: loadWindowWidth(id, 500),
1626 height: loadWindowHeight(id, 600),
1634 registerDragAndDrop();
1637 defaultEventType: "keydown",
1639 "ctrl+a": function(event) {
1640 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1642 if (event.target.isContentEditable)
1644 torrentsTable.selectAll();
1645 event.preventDefault();
1647 "delete": function(event) {
1648 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1650 if (event.target.isContentEditable)
1653 event.preventDefault();
1655 "shift+delete": (event) => {
1656 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1658 if (event.target.isContentEditable)
1661 event.preventDefault();
1667 window.addEventListener("load", () => {
1668 // fetch various data and store it in memory
1669 window.qBittorrent.Cache.buildInfo.init();
1670 window.qBittorrent.Cache.preferences.init();
1671 window.qBittorrent.Cache.qbtVersion.init();
1673 // switch to previously used tab
1674 const previouslyUsedTab = LocalPreferences.get("selected_tab", "transfers");
1675 switch (previouslyUsedTab) {
1677 if (window.qBittorrent.Client.isShowSearchEngine())
1678 $("searchTabLink").click();
1681 if (window.qBittorrent.Client.isShowRssReader())
1682 $("rssTabLink").click();
1685 if (window.qBittorrent.Client.isShowLogViewer())
1686 $("logTabLink").click();
1689 $("transfersTabLink").click();
1692 console.error(`Unexpected 'selected_tab' value: ${previouslyUsedTab}`);
1693 $("transfersTabLink").click();