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 = {};
31 window.qBittorrent.Client = (() => {
32 const exports = () => {
34 closeWindows: closeWindows,
36 getSyncMainDataInterval: getSyncMainDataInterval,
40 showSearchEngine: showSearchEngine,
41 showRssReader: showRssReader,
42 showLogViewer: showLogViewer,
43 isShowSearchEngine: isShowSearchEngine,
44 isShowRssReader: isShowRssReader,
45 isShowLogViewer: isShowLogViewer
49 const closeWindows = function() {
53 const genHash = function(string) {
55 // https://stackoverflow.com/a/8831937
56 // https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0
58 for (let i = 0; i < string.length; ++i)
59 hash = ((Math.imul(hash, 31) + string.charCodeAt(i)) | 0);
63 const getSyncMainDataInterval = function() {
64 return customSyncMainDataInterval ? customSyncMainDataInterval : serverSyncMainDataInterval;
68 const isStopped = () => {
76 const mainTitle = () => {
77 const emDash = "\u2014";
78 const qbtVersion = window.qBittorrent.Cache.qbtVersion.get();
79 const suffix = window.qBittorrent.Cache.preferences.get()["app_instance_name"] || "";
80 const title = `qBittorrent ${qbtVersion} QBT_TR(WebUI)QBT_TR[CONTEXT=OptionsDialog]`
81 + ((suffix.length > 0) ? ` ${emDash} ${suffix}` : "");
85 let showingSearchEngine = false;
86 let showingRssReader = false;
87 let showingLogViewer = false;
89 const showSearchEngine = function(bool) {
90 showingSearchEngine = bool;
92 const showRssReader = function(bool) {
93 showingRssReader = bool;
95 const showLogViewer = function(bool) {
96 showingLogViewer = bool;
98 const isShowSearchEngine = function() {
99 return showingSearchEngine;
101 const isShowRssReader = function() {
102 return showingRssReader;
104 const isShowLogViewer = function() {
105 return showingLogViewer;
110 Object.freeze(window.qBittorrent.Client);
112 // TODO: move global functions/variables into some namespace/scope
114 this.torrentsTable = new window.qBittorrent.DynamicTable.TorrentsTable();
116 let updatePropertiesPanel = function() {};
118 this.updateMainData = function() {};
119 let alternativeSpeedLimits = false;
120 let queueing_enabled = true;
121 let serverSyncMainDataInterval = 1500;
122 let customSyncMainDataInterval = null;
123 let useSubcategories = true;
125 /* Categories filter */
126 const CATEGORIES_ALL = 1;
127 const CATEGORIES_UNCATEGORIZED = 2;
129 const category_list = new Map();
131 let selected_category = Number(LocalPreferences.get("selected_category", CATEGORIES_ALL));
132 let setCategoryFilter = function() {};
136 const TAGS_UNTAGGED = 2;
138 const tagList = new Map();
140 let selectedTag = Number(LocalPreferences.get("selected_tag", TAGS_ALL));
141 let setTagFilter = function() {};
143 /* Trackers filter */
144 const TRACKERS_ALL = 1;
145 const TRACKERS_TRACKERLESS = 2;
147 /** @type Map<number, {host: string, trackerTorrentMap: Map<string, string[]>}> **/
148 const trackerList = new Map();
150 let selectedTracker = LocalPreferences.get("selected_tracker", TRACKERS_ALL);
151 let setTrackerFilter = function() {};
154 let selected_filter = LocalPreferences.get("selected_filter", "all");
155 let setFilter = function() {};
156 let toggleFilterDisplay = function() {};
158 window.addEventListener("DOMContentLoaded", () => {
159 let isSearchPanelLoaded = false;
160 let isLogPanelLoaded = false;
162 const saveColumnSizes = function() {
163 const filters_width = $("Filters").getSize().x;
164 LocalPreferences.set("filters_width", filters_width);
165 const properties_height_rel = $("propertiesPanel").getSize().y / Window.getSize().y;
166 LocalPreferences.set("properties_height_rel", properties_height_rel);
169 window.addEvent("resize", () => {
170 // only save sizes if the columns are visible
171 if (!$("mainColumn").hasClass("invisible"))
172 saveColumnSizes.delay(200); // Resizing might takes some time.
175 /* MochaUI.Desktop = new MochaUI.Desktop();
176 MochaUI.Desktop.desktop.setStyles({
177 'background': '#fff',
178 'visibility': 'visible'
180 MochaUI.Desktop.initialize();
182 const buildTransfersTab = function() {
183 const filt_w = Number(LocalPreferences.get("filters_width", 120));
187 onResize: saveColumnSizes,
189 resizeLimit: [1, 300]
197 const buildSearchTab = function() {
199 id: "searchTabColumn",
205 $("searchTabColumn").addClass("invisible");
208 const buildRssTab = function() {
216 $("rssTabColumn").addClass("invisible");
219 const buildLogTab = function() {
227 $("logTabColumn").addClass("invisible");
234 MochaUI.initializeTabs("mainWindowTabsList");
236 setCategoryFilter = function(hash) {
237 selected_category = hash;
238 LocalPreferences.set("selected_category", selected_category);
239 highlightSelectedCategory();
240 if (typeof torrentsTable.tableBody !== "undefined")
244 setTagFilter = function(hash) {
246 LocalPreferences.set("selected_tag", selectedTag);
247 highlightSelectedTag();
248 if (torrentsTable.tableBody !== undefined)
252 setTrackerFilter = function(hash) {
253 selectedTracker = hash.toString();
254 LocalPreferences.set("selected_tracker", selectedTracker);
255 highlightSelectedTracker();
256 if (torrentsTable.tableBody !== undefined)
260 setFilter = function(f) {
261 // Visually Select the right filter
262 $("all_filter").removeClass("selectedFilter");
263 $("downloading_filter").removeClass("selectedFilter");
264 $("seeding_filter").removeClass("selectedFilter");
265 $("completed_filter").removeClass("selectedFilter");
266 $("stopped_filter").removeClass("selectedFilter");
267 $("running_filter").removeClass("selectedFilter");
268 $("active_filter").removeClass("selectedFilter");
269 $("inactive_filter").removeClass("selectedFilter");
270 $("stalled_filter").removeClass("selectedFilter");
271 $("stalled_uploading_filter").removeClass("selectedFilter");
272 $("stalled_downloading_filter").removeClass("selectedFilter");
273 $("checking_filter").removeClass("selectedFilter");
274 $("moving_filter").removeClass("selectedFilter");
275 $("errored_filter").removeClass("selectedFilter");
276 $(f + "_filter").addClass("selectedFilter");
278 LocalPreferences.set("selected_filter", f);
280 if (typeof torrentsTable.tableBody !== "undefined")
284 toggleFilterDisplay = function(filter) {
285 const element = filter + "FilterList";
286 LocalPreferences.set("filter_" + filter + "_collapsed", !$(element).hasClass("invisible"));
287 $(element).toggleClass("invisible");
288 const parent = $(element).getParent(".filterWrapper");
289 const toggleIcon = $(parent).getChildren(".filterTitle img");
291 toggleIcon[0].toggleClass("rotate");
305 contentURL: "views/filters.html",
306 onContentLoaded: function() {
307 setFilter(selected_filter);
309 column: "filtersColumn",
314 // Show Top Toolbar is enabled by default
315 let showTopToolbar = LocalPreferences.get("show_top_toolbar", "true") === "true";
316 if (!showTopToolbar) {
317 $("showTopToolbarLink").firstChild.style.opacity = "0";
318 $("mochaToolbar").addClass("invisible");
321 // Show Status Bar is enabled by default
322 let showStatusBar = LocalPreferences.get("show_status_bar", "true") === "true";
323 if (!showStatusBar) {
324 $("showStatusBarLink").firstChild.style.opacity = "0";
325 $("desktopFooterWrapper").addClass("invisible");
328 // Show Filters Sidebar is enabled by default
329 let showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
330 if (!showFiltersSidebar) {
331 $("showFiltersSidebarLink").firstChild.style.opacity = "0";
332 $("filtersColumn").addClass("invisible");
333 $("filtersColumn_handle").addClass("invisible");
336 let speedInTitle = LocalPreferences.get("speed_in_browser_title_bar") === "true";
338 $("speedInBrowserTitleBarLink").firstChild.style.opacity = "0";
340 // After showing/hiding the toolbar + status bar
341 window.qBittorrent.Client.showSearchEngine(LocalPreferences.get("show_search_engine") !== "false");
342 window.qBittorrent.Client.showRssReader(LocalPreferences.get("show_rss_reader") !== "false");
343 window.qBittorrent.Client.showLogViewer(LocalPreferences.get("show_log_viewer") === "true");
345 // After Show Top Toolbar
346 MochaUI.Desktop.setDesktopSize();
348 let syncMainDataLastResponseId = 0;
349 const serverState = {};
351 const removeTorrentFromCategoryList = function(hash) {
356 category_list.forEach((category) => {
357 const deleteResult = category.torrents.delete(hash);
358 removed ||= deleteResult;
364 const addTorrentToCategoryList = function(torrent) {
365 const category = torrent["category"];
366 if (typeof category === "undefined")
369 const hash = torrent["hash"];
370 if (category.length === 0) { // Empty category
371 removeTorrentFromCategoryList(hash);
375 const categoryHash = window.qBittorrent.Client.genHash(category);
376 if (!category_list.has(categoryHash)) { // This should not happen
377 category_list.set(categoryHash, {
383 const torrents = category_list.get(categoryHash).torrents;
384 if (!torrents.has(hash)) {
385 removeTorrentFromCategoryList(hash);
392 const removeTorrentFromTagList = function(hash) {
397 tagList.forEach((tag) => {
398 const deleteResult = tag.torrents.delete(hash);
399 removed ||= deleteResult;
405 const addTorrentToTagList = function(torrent) {
406 if (torrent["tags"] === undefined) // Tags haven't changed
409 const hash = torrent["hash"];
410 removeTorrentFromTagList(hash);
412 if (torrent["tags"].length === 0) // No tags
415 const tags = torrent["tags"].split(",");
417 for (let i = 0; i < tags.length; ++i) {
418 const tagHash = window.qBittorrent.Client.genHash(tags[i].trim());
419 if (!tagList.has(tagHash)) { // This should not happen
420 tagList.set(tagHash, {
426 const torrents = tagList.get(tagHash).torrents;
427 if (!torrents.has(hash)) {
435 const updateFilter = function(filter, filterTitle) {
436 $(filter + "_filter").firstChild.childNodes[1].nodeValue = filterTitle.replace("%1", torrentsTable.getFilteredTorrentsNumber(filter, CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL));
439 const updateFiltersList = function() {
440 updateFilter("all", "QBT_TR(All (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
441 updateFilter("downloading", "QBT_TR(Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
442 updateFilter("seeding", "QBT_TR(Seeding (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
443 updateFilter("completed", "QBT_TR(Completed (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
444 updateFilter("running", "QBT_TR(Running (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
445 updateFilter("stopped", "QBT_TR(Stopped (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
446 updateFilter("active", "QBT_TR(Active (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
447 updateFilter("inactive", "QBT_TR(Inactive (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
448 updateFilter("stalled", "QBT_TR(Stalled (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
449 updateFilter("stalled_uploading", "QBT_TR(Stalled Uploading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
450 updateFilter("stalled_downloading", "QBT_TR(Stalled Downloading (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
451 updateFilter("checking", "QBT_TR(Checking (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
452 updateFilter("moving", "QBT_TR(Moving (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
453 updateFilter("errored", "QBT_TR(Errored (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
456 const updateCategoryList = function() {
457 const categoryList = $("categoryFilterList");
460 categoryList.getChildren().each(c => c.destroy());
462 const create_link = function(hash, text, count) {
463 let display_name = text;
465 if (useSubcategories) {
466 const category_path = text.split("/");
467 display_name = category_path[category_path.length - 1];
468 margin_left = (category_path.length - 1) * 20;
471 const html = `<span class="link" href="#" style="margin-left: ${margin_left}px;" onclick="setCategoryFilter(${hash}); return false;">`
472 + '<img src="images/view-categories.svg"/>'
473 + window.qBittorrent.Misc.escapeHtml(display_name) + " (" + count + ")" + "</span>";
474 const el = new Element("li", {
478 window.qBittorrent.Filters.categoriesFilterContextMenu.addTarget(el);
482 const all = torrentsTable.getRowIds().length;
483 let uncategorized = 0;
484 for (const key in torrentsTable.rows) {
485 if (!Object.hasOwn(torrentsTable.rows, key))
488 const row = torrentsTable.rows[key];
489 if (row["full_data"].category.length === 0)
492 categoryList.appendChild(create_link(CATEGORIES_ALL, "QBT_TR(All)QBT_TR[CONTEXT=CategoryFilterModel]", all));
493 categoryList.appendChild(create_link(CATEGORIES_UNCATEGORIZED, "QBT_TR(Uncategorized)QBT_TR[CONTEXT=CategoryFilterModel]", uncategorized));
495 const sortedCategories = [];
496 category_list.forEach((category, hash) => sortedCategories.push({
497 categoryName: category.name,
499 categoryCount: category.torrents.size
501 sortedCategories.sort((left, right) => {
502 const leftSegments = left.categoryName.split("/");
503 const rightSegments = right.categoryName.split("/");
505 for (let i = 0, iMax = Math.min(leftSegments.length, rightSegments.length); i < iMax; ++i) {
506 const compareResult = window.qBittorrent.Misc.naturalSortCollator.compare(
507 leftSegments[i], rightSegments[i]);
508 if (compareResult !== 0)
509 return compareResult;
512 return leftSegments.length - rightSegments.length;
515 for (let i = 0; i < sortedCategories.length; ++i) {
516 const { categoryName, categoryHash } = sortedCategories[i];
517 let { categoryCount } = sortedCategories[i];
519 if (useSubcategories) {
520 for (let j = (i + 1);
521 ((j < sortedCategories.length) && sortedCategories[j].categoryName.startsWith(categoryName + "/")); ++j)
522 categoryCount += sortedCategories[j].categoryCount;
525 categoryList.appendChild(create_link(categoryHash, categoryName, categoryCount));
528 highlightSelectedCategory();
531 const highlightSelectedCategory = function() {
532 const categoryList = $("categoryFilterList");
535 const children = categoryList.childNodes;
536 for (let i = 0; i < children.length; ++i) {
537 if (Number(children[i].id) === selected_category)
538 children[i].className = "selectedFilter";
540 children[i].className = "";
544 const updateTagList = function() {
545 const tagFilterList = $("tagFilterList");
546 if (tagFilterList === null)
549 tagFilterList.getChildren().each(c => c.destroy());
551 const createLink = function(hash, text, count) {
552 const html = `<span class="link" href="#" onclick="setTagFilter(${hash}); return false;">`
553 + '<img src="images/tags.svg"/>'
554 + window.qBittorrent.Misc.escapeHtml(text) + " (" + count + ")" + "</span>";
555 const el = new Element("li", {
559 window.qBittorrent.Filters.tagsFilterContextMenu.addTarget(el);
563 const torrentsCount = torrentsTable.getRowIds().length;
565 for (const key in torrentsTable.rows) {
566 if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]["full_data"].tags.length === 0))
569 tagFilterList.appendChild(createLink(TAGS_ALL, "QBT_TR(All)QBT_TR[CONTEXT=TagFilterModel]", torrentsCount));
570 tagFilterList.appendChild(createLink(TAGS_UNTAGGED, "QBT_TR(Untagged)QBT_TR[CONTEXT=TagFilterModel]", untagged));
572 const sortedTags = [];
573 tagList.forEach((tag, hash) => sortedTags.push({
576 tagSize: tag.torrents.size
578 sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName));
580 for (const { tagName, tagHash, tagSize } of sortedTags)
581 tagFilterList.appendChild(createLink(tagHash, tagName, tagSize));
583 highlightSelectedTag();
586 const highlightSelectedTag = function() {
587 const tagFilterList = $("tagFilterList");
591 const children = tagFilterList.childNodes;
592 for (let i = 0; i < children.length; ++i)
593 children[i].className = (Number(children[i].id) === selectedTag) ? "selectedFilter" : "";
596 // getHost emulate the GUI version `QString getHost(const QString &url)`
597 const getHost = function(url) {
598 // We want the hostname.
599 // If failed to parse the domain, original input should be returned
601 if (!/^(?:https?|udp):/i.test(url))
605 // hack: URL can not get hostname from udp protocol
606 const parsedUrl = new URL(url.replace(/^udp:/i, "https:"));
607 // host: "example.com:8443"
608 // hostname: "example.com"
609 const host = parsedUrl.hostname;
620 const updateTrackerList = function() {
621 const trackerFilterList = $("trackerFilterList");
622 if (trackerFilterList === null)
625 trackerFilterList.getChildren().each(c => c.destroy());
627 const createLink = function(hash, text, count) {
628 const html = '<span class="link" href="#" onclick="setTrackerFilter(' + hash + ');return false;">'
629 + '<img src="images/trackers.svg"/>'
630 + window.qBittorrent.Misc.escapeHtml(text.replace("%1", count)) + "</span>";
631 const el = new Element("li", {
635 window.qBittorrent.Filters.trackersFilterContextMenu.addTarget(el);
639 const torrentsCount = torrentsTable.getRowIds().length;
640 trackerFilterList.appendChild(createLink(TRACKERS_ALL, "QBT_TR(All (%1))QBT_TR[CONTEXT=TrackerFiltersList]", torrentsCount));
641 let trackerlessTorrentsCount = 0;
642 for (const key in torrentsTable.rows) {
643 if (Object.hasOwn(torrentsTable.rows, key) && (torrentsTable.rows[key]["full_data"].trackers_count === 0))
644 trackerlessTorrentsCount += 1;
646 trackerFilterList.appendChild(createLink(TRACKERS_TRACKERLESS, "QBT_TR(Trackerless (%1))QBT_TR[CONTEXT=TrackerFiltersList]", trackerlessTorrentsCount));
648 // Sort trackers by hostname
649 const sortedList = [];
650 trackerList.forEach(({ host, trackerTorrentMap }, hash) => {
651 const uniqueTorrents = new Set();
652 for (const torrents of trackerTorrentMap.values()) {
653 for (const torrent of torrents)
654 uniqueTorrents.add(torrent);
660 trackerCount: uniqueTorrents.size,
663 sortedList.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.trackerHost, right.trackerHost));
664 for (const { trackerHost, trackerHash, trackerCount } of sortedList)
665 trackerFilterList.appendChild(createLink(trackerHash, (trackerHost + " (%1)"), trackerCount));
667 highlightSelectedTracker();
670 const highlightSelectedTracker = function() {
671 const trackerFilterList = $("trackerFilterList");
672 if (!trackerFilterList)
675 const children = trackerFilterList.childNodes;
676 for (const child of children)
677 child.className = (child.id === selectedTracker) ? "selectedFilter" : "";
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;
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.set("html", "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]");
723 syncRequestInProgress = false;
726 onSuccess: function(response) {
727 $("error_div").set("html", "");
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.Client.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.Client.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.Client.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.Client.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 = getHost(tracker);
800 const hash = window.qBittorrent.Client.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 hash = window.qBittorrent.Client.genHash(getHost(tracker));
816 const trackerListEntry = trackerList.get(hash);
817 if (trackerListEntry)
818 trackerListEntry.trackerTorrentMap.delete(tracker);
820 updateTrackers = true;
822 if (response["torrents"]) {
823 let updateTorrentList = false;
824 for (const key in response["torrents"]) {
825 if (!Object.hasOwn(response["torrents"], key))
828 response["torrents"][key]["hash"] = key;
829 response["torrents"][key]["rowId"] = key;
830 if (response["torrents"][key]["state"])
831 response["torrents"][key]["status"] = response["torrents"][key]["state"];
832 torrentsTable.updateRowData(response["torrents"][key]);
833 if (addTorrentToCategoryList(response["torrents"][key]))
834 update_categories = true;
835 if (addTorrentToTagList(response["torrents"][key]))
837 if (response["torrents"][key]["name"])
838 updateTorrentList = true;
841 if (updateTorrentList)
842 setupCopyEventHandler();
844 if (response["torrents_removed"]) {
845 response["torrents_removed"].each((hash) => {
846 torrentsTable.removeRow(hash);
847 removeTorrentFromCategoryList(hash);
848 update_categories = true; // Always to update All category
849 removeTorrentFromTagList(hash);
850 updateTags = true; // Always to update All tag
853 torrentsTable.updateTable(full_update);
854 torrentsTable.altRow();
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);
899 if (window.qBittorrent.Client.isStopped())
902 syncMainDataTimeoutID = syncMainData.delay(delay);
905 const processServerState = function() {
906 let transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true);
907 if (serverState.dl_rate_limit > 0)
908 transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_rate_limit, true) + "]";
909 transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_data, false) + ")";
910 $("DlInfos").set("html", transfer_info);
911 transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true);
912 if (serverState.up_rate_limit > 0)
913 transfer_info += " [" + window.qBittorrent.Misc.friendlyUnit(serverState.up_rate_limit, true) + "]";
914 transfer_info += " (" + window.qBittorrent.Misc.friendlyUnit(serverState.up_info_data, false) + ")";
915 $("UpInfos").set("html", transfer_info);
917 document.title = (speedInTitle
918 ? (`QBT_TR([D: %1, U: %2])QBT_TR[CONTEXT=MainWindow] `
919 .replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_speed, true))
920 .replace("%2", window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true)))
922 + window.qBittorrent.Client.mainTitle();
924 $("freeSpaceOnDisk").set("html", "QBT_TR(Free space: %1)QBT_TR[CONTEXT=HttpServer]".replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.free_space_on_disk)));
925 $("DHTNodes").set("html", "QBT_TR(DHT: %1 nodes)QBT_TR[CONTEXT=StatusBar]".replace("%1", serverState.dht_nodes));
928 if (document.getElementById("statisticsContent")) {
929 $("AlltimeDL").set("html", window.qBittorrent.Misc.friendlyUnit(serverState.alltime_dl, false));
930 $("AlltimeUL").set("html", window.qBittorrent.Misc.friendlyUnit(serverState.alltime_ul, false));
931 $("TotalWastedSession").set("html", window.qBittorrent.Misc.friendlyUnit(serverState.total_wasted_session, false));
932 $("GlobalRatio").set("html", serverState.global_ratio);
933 $("TotalPeerConnections").set("html", serverState.total_peer_connections);
934 $("ReadCacheHits").set("html", serverState.read_cache_hits + "%");
935 $("TotalBuffersSize").set("html", window.qBittorrent.Misc.friendlyUnit(serverState.total_buffers_size, false));
936 $("WriteCacheOverload").set("html", serverState.write_cache_overload + "%");
937 $("ReadCacheOverload").set("html", serverState.read_cache_overload + "%");
938 $("QueuedIOJobs").set("html", serverState.queued_io_jobs);
939 $("AverageTimeInQueue").set("html", serverState.average_time_queue + " ms");
940 $("TotalQueuedSize").set("html", window.qBittorrent.Misc.friendlyUnit(serverState.total_queued_size, false));
943 switch (serverState.connection_status) {
945 $("connectionStatus").src = "images/connected.svg";
946 $("connectionStatus").alt = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
947 $("connectionStatus").title = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
950 $("connectionStatus").src = "images/firewalled.svg";
951 $("connectionStatus").alt = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
952 $("connectionStatus").title = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
955 $("connectionStatus").src = "images/disconnected.svg";
956 $("connectionStatus").alt = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
957 $("connectionStatus").title = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
961 if (queueing_enabled !== serverState.queueing) {
962 queueing_enabled = serverState.queueing;
963 torrentsTable.columns["priority"].force_hide = !queueing_enabled;
964 torrentsTable.updateColumn("priority");
965 if (queueing_enabled) {
966 $("topQueuePosItem").removeClass("invisible");
967 $("increaseQueuePosItem").removeClass("invisible");
968 $("decreaseQueuePosItem").removeClass("invisible");
969 $("bottomQueuePosItem").removeClass("invisible");
970 $("queueingButtons").removeClass("invisible");
971 $("queueingMenuItems").removeClass("invisible");
974 $("topQueuePosItem").addClass("invisible");
975 $("increaseQueuePosItem").addClass("invisible");
976 $("decreaseQueuePosItem").addClass("invisible");
977 $("bottomQueuePosItem").addClass("invisible");
978 $("queueingButtons").addClass("invisible");
979 $("queueingMenuItems").addClass("invisible");
983 if (alternativeSpeedLimits !== serverState.use_alt_speed_limits) {
984 alternativeSpeedLimits = serverState.use_alt_speed_limits;
985 updateAltSpeedIcon(alternativeSpeedLimits);
988 if (useSubcategories !== serverState.use_subcategories) {
989 useSubcategories = serverState.use_subcategories;
990 updateCategoryList();
993 serverSyncMainDataInterval = Math.max(serverState.refresh_interval, 500);
996 const updateAltSpeedIcon = function(enabled) {
998 $("alternativeSpeedLimits").src = "images/slow.svg";
999 $("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
1000 $("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
1003 $("alternativeSpeedLimits").src = "images/slow_off.svg";
1004 $("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
1005 $("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
1009 $("alternativeSpeedLimits").addEvent("click", () => {
1010 // Change icon immediately to give some feedback
1011 updateAltSpeedIcon(!alternativeSpeedLimits);
1014 url: "api/v2/transfer/toggleSpeedLimitsMode",
1016 onComplete: function() {
1017 alternativeSpeedLimits = !alternativeSpeedLimits;
1020 onFailure: function() {
1021 // Restore icon in case of failure
1022 updateAltSpeedIcon(alternativeSpeedLimits);
1027 $("DlInfos").addEvent("click", globalDownloadLimitFN);
1028 $("UpInfos").addEvent("click", globalUploadLimitFN);
1030 $("showTopToolbarLink").addEvent("click", (e) => {
1031 showTopToolbar = !showTopToolbar;
1032 LocalPreferences.set("show_top_toolbar", showTopToolbar.toString());
1033 if (showTopToolbar) {
1034 $("showTopToolbarLink").firstChild.style.opacity = "1";
1035 $("mochaToolbar").removeClass("invisible");
1038 $("showTopToolbarLink").firstChild.style.opacity = "0";
1039 $("mochaToolbar").addClass("invisible");
1041 MochaUI.Desktop.setDesktopSize();
1044 $("showStatusBarLink").addEvent("click", (e) => {
1045 showStatusBar = !showStatusBar;
1046 LocalPreferences.set("show_status_bar", showStatusBar.toString());
1047 if (showStatusBar) {
1048 $("showStatusBarLink").firstChild.style.opacity = "1";
1049 $("desktopFooterWrapper").removeClass("invisible");
1052 $("showStatusBarLink").firstChild.style.opacity = "0";
1053 $("desktopFooterWrapper").addClass("invisible");
1055 MochaUI.Desktop.setDesktopSize();
1058 const registerMagnetHandler = function() {
1059 if (typeof navigator.registerProtocolHandler !== "function") {
1060 if (window.location.protocol !== "https:")
1061 alert("QBT_TR(To use this feature, the WebUI needs to be accessed over HTTPS)QBT_TR[CONTEXT=MainWindow]");
1063 alert("QBT_TR(Your browser does not support this feature)QBT_TR[CONTEXT=MainWindow]");
1067 const hashString = location.hash ? location.hash.replace(/^#/, "") : "";
1068 const hashParams = new URLSearchParams(hashString);
1069 hashParams.set("download", "");
1071 const templateHashString = hashParams.toString().replace("download=", "download=%s");
1072 const templateUrl = location.origin + location.pathname
1073 + location.search + "#" + templateHashString;
1075 navigator.registerProtocolHandler("magnet", templateUrl,
1076 "qBittorrent WebUI magnet handler");
1078 $("registerMagnetHandlerLink").addEvent("click", (e) => {
1079 registerMagnetHandler();
1082 $("showFiltersSidebarLink").addEvent("click", (e) => {
1083 showFiltersSidebar = !showFiltersSidebar;
1084 LocalPreferences.set("show_filters_sidebar", showFiltersSidebar.toString());
1085 if (showFiltersSidebar) {
1086 $("showFiltersSidebarLink").firstChild.style.opacity = "1";
1087 $("filtersColumn").removeClass("invisible");
1088 $("filtersColumn_handle").removeClass("invisible");
1091 $("showFiltersSidebarLink").firstChild.style.opacity = "0";
1092 $("filtersColumn").addClass("invisible");
1093 $("filtersColumn_handle").addClass("invisible");
1095 MochaUI.Desktop.setDesktopSize();
1098 $("speedInBrowserTitleBarLink").addEvent("click", (e) => {
1099 speedInTitle = !speedInTitle;
1100 LocalPreferences.set("speed_in_browser_title_bar", speedInTitle.toString());
1102 $("speedInBrowserTitleBarLink").firstChild.style.opacity = "1";
1104 $("speedInBrowserTitleBarLink").firstChild.style.opacity = "0";
1105 processServerState();
1108 $("showSearchEngineLink").addEvent("click", (e) => {
1109 window.qBittorrent.Client.showSearchEngine(!window.qBittorrent.Client.isShowSearchEngine());
1110 LocalPreferences.set("show_search_engine", window.qBittorrent.Client.isShowSearchEngine().toString());
1114 $("showRssReaderLink").addEvent("click", (e) => {
1115 window.qBittorrent.Client.showRssReader(!window.qBittorrent.Client.isShowRssReader());
1116 LocalPreferences.set("show_rss_reader", window.qBittorrent.Client.isShowRssReader().toString());
1120 $("showLogViewerLink").addEvent("click", (e) => {
1121 window.qBittorrent.Client.showLogViewer(!window.qBittorrent.Client.isShowLogViewer());
1122 LocalPreferences.set("show_log_viewer", window.qBittorrent.Client.isShowLogViewer().toString());
1126 const updateTabDisplay = function() {
1127 if (window.qBittorrent.Client.isShowRssReader()) {
1128 $("showRssReaderLink").firstChild.style.opacity = "1";
1129 $("mainWindowTabs").removeClass("invisible");
1130 $("rssTabLink").removeClass("invisible");
1131 if (!MochaUI.Panels.instances.RssPanel)
1135 $("showRssReaderLink").firstChild.style.opacity = "0";
1136 $("rssTabLink").addClass("invisible");
1137 if ($("rssTabLink").hasClass("selected"))
1138 $("transfersTabLink").click();
1141 if (window.qBittorrent.Client.isShowSearchEngine()) {
1142 $("showSearchEngineLink").firstChild.style.opacity = "1";
1143 $("mainWindowTabs").removeClass("invisible");
1144 $("searchTabLink").removeClass("invisible");
1145 if (!MochaUI.Panels.instances.SearchPanel)
1149 $("showSearchEngineLink").firstChild.style.opacity = "0";
1150 $("searchTabLink").addClass("invisible");
1151 if ($("searchTabLink").hasClass("selected"))
1152 $("transfersTabLink").click();
1155 if (window.qBittorrent.Client.isShowLogViewer()) {
1156 $("showLogViewerLink").firstChild.style.opacity = "1";
1157 $("mainWindowTabs").removeClass("invisible");
1158 $("logTabLink").removeClass("invisible");
1159 if (!MochaUI.Panels.instances.LogPanel)
1163 $("showLogViewerLink").firstChild.style.opacity = "0";
1164 $("logTabLink").addClass("invisible");
1165 if ($("logTabLink").hasClass("selected"))
1166 $("transfersTabLink").click();
1170 if (!window.qBittorrent.Client.isShowRssReader() && !window.qBittorrent.Client.isShowSearchEngine() && !window.qBittorrent.Client.isShowLogViewer())
1171 $("mainWindowTabs").addClass("invisible");
1174 $("StatisticsLink").addEvent("click", StatisticsLinkFN);
1178 const showTransfersTab = function() {
1179 const showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
1180 if (showFiltersSidebar) {
1181 $("filtersColumn").removeClass("invisible");
1182 $("filtersColumn_handle").removeClass("invisible");
1184 $("mainColumn").removeClass("invisible");
1185 $("torrentsFilterToolbar").removeClass("invisible");
1187 customSyncMainDataInterval = null;
1194 LocalPreferences.set("selected_window_tab", "transfers");
1197 const hideTransfersTab = function() {
1198 $("filtersColumn").addClass("invisible");
1199 $("filtersColumn_handle").addClass("invisible");
1200 $("mainColumn").addClass("invisible");
1201 $("torrentsFilterToolbar").addClass("invisible");
1202 MochaUI.Desktop.resizePanels();
1205 const showSearchTab = (function() {
1206 let searchTabInitialized = false;
1209 // we must wait until the panel is fully loaded before proceeding.
1210 // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1211 // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1212 if (!isSearchPanelLoaded) {
1219 if (!searchTabInitialized) {
1220 window.qBittorrent.Search.init();
1221 searchTabInitialized = true;
1224 $("searchTabColumn").removeClass("invisible");
1225 customSyncMainDataInterval = 30000;
1230 LocalPreferences.set("selected_window_tab", "search");
1234 const hideSearchTab = function() {
1235 $("searchTabColumn").addClass("invisible");
1236 MochaUI.Desktop.resizePanels();
1239 const showRssTab = (function() {
1240 let rssTabInitialized = false;
1243 if (!rssTabInitialized) {
1244 window.qBittorrent.Rss.init();
1245 rssTabInitialized = true;
1248 window.qBittorrent.Rss.load();
1251 $("rssTabColumn").removeClass("invisible");
1252 customSyncMainDataInterval = 30000;
1257 LocalPreferences.set("selected_window_tab", "rss");
1261 const hideRssTab = function() {
1262 $("rssTabColumn").addClass("invisible");
1263 window.qBittorrent.Rss && window.qBittorrent.Rss.unload();
1264 MochaUI.Desktop.resizePanels();
1267 const showLogTab = (function() {
1268 let logTabInitialized = false;
1271 // we must wait until the panel is fully loaded before proceeding.
1272 // this include's the panel's custom js, which is loaded via MochaUI.Panel's 'require' field.
1273 // MochaUI loads these files asynchronously and thus all required libs may not be available immediately
1274 if (!isLogPanelLoaded) {
1281 if (!logTabInitialized) {
1282 window.qBittorrent.Log.init();
1283 logTabInitialized = true;
1286 window.qBittorrent.Log.load();
1289 $("logTabColumn").removeClass("invisible");
1290 customSyncMainDataInterval = 30000;
1295 LocalPreferences.set("selected_window_tab", "log");
1299 const hideLogTab = function() {
1300 $("logTabColumn").addClass("invisible");
1301 MochaUI.Desktop.resizePanels();
1302 window.qBittorrent.Log && window.qBittorrent.Log.unload();
1305 const addSearchPanel = function() {
1317 contentURL: "views/search.html",
1319 js: ["scripts/search.js"],
1321 isSearchPanelLoaded = true;
1325 column: "searchTabColumn",
1330 const addRssPanel = function() {
1342 contentURL: "views/rss.html",
1344 column: "rssTabColumn",
1349 const addLogPanel = function() {
1361 contentURL: "views/log.html",
1363 css: ["css/vanillaSelectBox.css"],
1364 js: ["scripts/lib/vanillaSelectBox.js"],
1366 isLogPanelLoaded = true;
1369 tabsURL: "views/logTabs.html",
1370 tabsOnload: function() {
1371 MochaUI.initializeTabs("panelTabs");
1373 $("logMessageLink").addEvent("click", (e) => {
1374 window.qBittorrent.Log.setCurrentTab("main");
1377 $("logPeerLink").addEvent("click", (e) => {
1378 window.qBittorrent.Log.setCurrentTab("peer");
1383 column: "logTabColumn",
1388 const handleDownloadParam = function() {
1389 // Extract torrent URL from download param in WebUI URL hash
1390 const downloadHash = "#download=";
1391 if (location.hash.indexOf(downloadHash) !== 0)
1394 const url = decodeURIComponent(location.hash.substring(downloadHash.length));
1395 // Remove the processed hash from the URL
1396 history.replaceState("", document.title, (location.pathname + location.search));
1397 showDownloadPage([url]);
1411 contentURL: "views/transferlist.html",
1412 onContentLoaded: function() {
1413 handleDownloadParam();
1416 column: "mainColumn",
1417 onResize: saveColumnSizes,
1420 let prop_h = LocalPreferences.get("properties_height_rel");
1421 if (prop_h !== null)
1422 prop_h = prop_h.toFloat() * Window.getSize().y;
1424 prop_h = Window.getSize().y / 2.0;
1426 id: "propertiesPanel",
1435 contentURL: "views/properties.html",
1437 css: ["css/Tabs.css", "css/dynamicTable.css"],
1438 js: ["scripts/prop-general.js", "scripts/prop-trackers.js", "scripts/prop-peers.js", "scripts/prop-webseeds.js", "scripts/prop-files.js"],
1440 tabsURL: "views/propertiesToolbar.html",
1441 tabsOnload: function() {
1442 MochaUI.initializeTabs("propertiesTabs");
1444 updatePropertiesPanel = function() {
1445 if (!$("prop_general").hasClass("invisible")) {
1446 if (window.qBittorrent.PropGeneral !== undefined)
1447 window.qBittorrent.PropGeneral.updateData();
1449 else if (!$("prop_trackers").hasClass("invisible")) {
1450 if (window.qBittorrent.PropTrackers !== undefined)
1451 window.qBittorrent.PropTrackers.updateData();
1453 else if (!$("prop_peers").hasClass("invisible")) {
1454 if (window.qBittorrent.PropPeers !== undefined)
1455 window.qBittorrent.PropPeers.updateData();
1457 else if (!$("prop_webseeds").hasClass("invisible")) {
1458 if (window.qBittorrent.PropWebseeds !== undefined)
1459 window.qBittorrent.PropWebseeds.updateData();
1461 else if (!$("prop_files").hasClass("invisible")) {
1462 if (window.qBittorrent.PropFiles !== undefined)
1463 window.qBittorrent.PropFiles.updateData();
1467 $("PropGeneralLink").addEvent("click", function(e) {
1468 $$(".propertiesTabContent").addClass("invisible");
1469 $("prop_general").removeClass("invisible");
1471 updatePropertiesPanel();
1472 LocalPreferences.set("selected_tab", this.id);
1475 $("PropTrackersLink").addEvent("click", function(e) {
1476 $$(".propertiesTabContent").addClass("invisible");
1477 $("prop_trackers").removeClass("invisible");
1479 updatePropertiesPanel();
1480 LocalPreferences.set("selected_tab", this.id);
1483 $("PropPeersLink").addEvent("click", function(e) {
1484 $$(".propertiesTabContent").addClass("invisible");
1485 $("prop_peers").removeClass("invisible");
1487 updatePropertiesPanel();
1488 LocalPreferences.set("selected_tab", this.id);
1491 $("PropWebSeedsLink").addEvent("click", function(e) {
1492 $$(".propertiesTabContent").addClass("invisible");
1493 $("prop_webseeds").removeClass("invisible");
1495 updatePropertiesPanel();
1496 LocalPreferences.set("selected_tab", this.id);
1499 $("PropFilesLink").addEvent("click", function(e) {
1500 $$(".propertiesTabContent").addClass("invisible");
1501 $("prop_files").removeClass("invisible");
1503 updatePropertiesPanel();
1504 LocalPreferences.set("selected_tab", this.id);
1507 $("propertiesPanel_collapseToggle").addEvent("click", (e) => {
1508 updatePropertiesPanel();
1511 column: "mainColumn",
1515 const showFilesFilter = function() {
1516 $("torrentFilesFilterToolbar").removeClass("invisible");
1519 const hideFilesFilter = function() {
1520 $("torrentFilesFilterToolbar").addClass("invisible");
1523 // listen for changes to torrentsFilterInput
1524 let torrentsFilterInputTimer = -1;
1525 $("torrentsFilterInput").addEvent("input", () => {
1526 clearTimeout(torrentsFilterInputTimer);
1527 torrentsFilterInputTimer = setTimeout(() => {
1528 torrentsFilterInputTimer = -1;
1529 torrentsTable.updateTable();
1530 }, window.qBittorrent.Misc.FILTER_INPUT_DELAY);
1532 $("torrentsFilterRegexBox").addEvent("change", () => {
1533 torrentsTable.updateTable();
1536 $("transfersTabLink").addEvent("click", showTransfersTab);
1537 $("searchTabLink").addEvent("click", showSearchTab);
1538 $("rssTabLink").addEvent("click", showRssTab);
1539 $("logTabLink").addEvent("click", showLogTab);
1542 const registerDragAndDrop = () => {
1543 $("desktop").addEventListener("dragover", (ev) => {
1544 if (ev.preventDefault)
1545 ev.preventDefault();
1548 $("desktop").addEventListener("dragenter", (ev) => {
1549 if (ev.preventDefault)
1550 ev.preventDefault();
1553 $("desktop").addEventListener("drop", (ev) => {
1554 if (ev.preventDefault)
1555 ev.preventDefault();
1557 const droppedFiles = ev.dataTransfer.files;
1559 if (droppedFiles.length > 0) {
1560 // dropped files or folders
1562 // can't handle folder due to cannot put the filelist (from dropped folder)
1563 // to <input> `files` field
1564 for (const item of ev.dataTransfer.items) {
1565 if (item.webkitGetAsEntry().isDirectory)
1569 const id = "uploadPage";
1570 new MochaUI.Window({
1572 title: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
1573 loadMethod: "iframe",
1574 contentURL: new URI("upload.html").toString(),
1575 addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
1579 paddingHorizontal: 0,
1580 width: loadWindowWidth(id, 500),
1581 height: loadWindowHeight(id, 460),
1585 onContentLoaded: () => {
1586 const fileInput = $(`${id}_iframe`).contentDocument.getElementById("fileselect");
1587 fileInput.files = droppedFiles;
1592 const droppedText = ev.dataTransfer.getData("text");
1593 if (droppedText.length > 0) {
1596 const urls = droppedText.split("\n")
1597 .map((str) => str.trim())
1599 const lowercaseStr = str.toLowerCase();
1600 return lowercaseStr.startsWith("http:")
1601 || lowercaseStr.startsWith("https:")
1602 || lowercaseStr.startsWith("magnet:")
1603 || ((str.length === 40) && !(/[^0-9A-F]/i.test(str))) // v1 hex-encoded SHA-1 info-hash
1604 || ((str.length === 32) && !(/[^2-7A-Z]/i.test(str))); // v1 Base32 encoded SHA-1 info-hash
1607 if (urls.length <= 0)
1610 const id = "downloadPage";
1611 const contentURI = new URI("download.html").setData("urls", urls.map(encodeURIComponent).join("|"));
1612 new MochaUI.Window({
1614 title: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
1615 loadMethod: "iframe",
1616 contentURL: contentURI.toString(),
1617 addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
1622 paddingHorizontal: 0,
1623 width: loadWindowWidth(id, 500),
1624 height: loadWindowHeight(id, 600),
1632 registerDragAndDrop();
1635 defaultEventType: "keydown",
1637 "ctrl+a": function(event) {
1638 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1640 if (event.target.isContentEditable)
1642 torrentsTable.selectAll();
1643 event.preventDefault();
1645 "delete": function(event) {
1646 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1648 if (event.target.isContentEditable)
1651 event.preventDefault();
1653 "shift+delete": (event) => {
1654 if ((event.target.nodeName === "INPUT") || (event.target.nodeName === "TEXTAREA"))
1656 if (event.target.isContentEditable)
1659 event.preventDefault();
1665 window.addEventListener("load", () => {
1666 // fetch various data and store it in memory
1667 window.qBittorrent.Cache.buildInfo.init();
1668 window.qBittorrent.Cache.preferences.init();
1669 window.qBittorrent.Cache.qbtVersion.init();
1671 // switch to previously used tab
1672 const previouslyUsedTab = LocalPreferences.get("selected_window_tab", "transfers");
1673 switch (previouslyUsedTab) {
1675 if (window.qBittorrent.Client.isShowSearchEngine())
1676 $("searchTabLink").click();
1679 if (window.qBittorrent.Client.isShowRssReader())
1680 $("rssTabLink").click();
1683 if (window.qBittorrent.Client.isShowLogViewer())
1684 $("logTabLink").click();
1687 $("transfersTabLink").click();
1690 console.error(`Unexpected 'selected_window_tab' value: ${previouslyUsedTab}`);
1691 $("transfersTabLink").click();